feat 多通知渠道支持

This commit is contained in:
jxxghp 2023-07-14 13:05:58 +08:00
parent c1e8b6d0ff
commit 6d2f4697b0
22 changed files with 359 additions and 255 deletions

View File

@ -222,5 +222,5 @@ docker pull jxxghp/moviepilot:latest
- [x] 搜索结果过滤 - [x] 搜索结果过滤
- [ ] 媒体详情页面 - [ ] 媒体详情页面
- [ ] 洗版支持 - [ ] 洗版支持
- [ ] 多通知渠道支持 - [x] 多通知渠道支持

View File

@ -55,7 +55,7 @@ def add_downloading(
@router.post("/notexists", summary="查询缺失媒体信息", response_model=List[NotExistMediaInfo]) @router.post("/notexists", summary="查询缺失媒体信息", response_model=List[NotExistMediaInfo])
def exists(media_in: schemas.MediaInfo, def exists(media_in: schemas.MediaInfo,
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
查询缺失媒体信息 查询缺失媒体信息
""" """

View File

@ -1,7 +1,7 @@
import json import json
import json import json
import time import time
from typing import Any, List, Union from typing import Union
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse

View File

@ -10,8 +10,8 @@ from app.core.event import EventManager
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.core.module import ModuleManager from app.core.module import ModuleManager
from app.log import logger from app.log import logger
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification
from app.schemas.types import TorrentStatus, MediaType, MediaImageType, MessageChannel from app.schemas.types import TorrentStatus, MediaType, MediaImageType
from app.utils.object import ObjectUtils from app.utils.object import ObjectUtils
from app.utils.singleton import AbstractSingleton, Singleton from app.utils.singleton import AbstractSingleton, Singleton
@ -287,42 +287,31 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
""" """
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path) return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
def post_message(self, title: str, text: str = None, def post_message(self, message: Notification) -> Optional[bool]:
image: str = None, userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送消息 发送消息
:param title: 标题 :param message: 消息体
:param text: 内容
:param image: 图片
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.run_module("post_message", title=title, text=text, image=image, userid=userid) return self.run_module("post_message", message=message)
def post_medias_message(self, title: str, items: List[MediaInfo], def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送媒体信息选择列表 发送媒体信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param medias: 媒体列表
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.run_module("post_medias_message", title=title, items=items, userid=userid) return self.run_module("post_medias_message", message=message, medias=medias)
def post_torrents_message(self, title: str, items: List[Context], def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
mediainfo: MediaInfo,
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送种子信息选择列表 发送种子信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param torrents: 种子列表
:param mediainfo: 媒体信息
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.run_module("post_torrents_message", title=title, mediainfo=mediainfo, return self.run_module("post_torrents_message", message=message, torrents=torrents)
items=items, userid=userid)
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None: def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
""" """

View File

@ -13,6 +13,7 @@ from app.helper.cookiecloud import CookieCloudHelper
from app.helper.message import MessageHelper from app.helper.message import MessageHelper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.log import logger from app.log import logger
from app.schemas import Notification, NotificationType, MessageChannel
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
@ -34,17 +35,20 @@ class CookieCloudChain(ChainBase):
password=settings.COOKIECLOUD_PASSWORD password=settings.COOKIECLOUD_PASSWORD
) )
def remote_sync(self, userid: Union[int, str]): def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
""" """
远程触发同步站点发送消息 远程触发同步站点发送消息
""" """
self.post_message(title="开始同步CookieCloud站点 ...", userid=userid) self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title="开始同步CookieCloud站点 ...", userid=userid))
# 开始同步 # 开始同步
success, msg = self.process() success, msg = self.process()
if success: if success:
self.post_message(title=f"同步站点成功,{msg}", userid=userid) self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title=f"同步站点成功,{msg}", userid=userid))
else: else:
self.post_message(title=f"同步站点失败:{msg}", userid=userid) self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title=f"同步站点失败:{msg}", userid=userid))
def process(self, manual=False) -> Tuple[bool, str]: def process(self, manual=False) -> Tuple[bool, str]:
""" """

View File

@ -12,7 +12,7 @@ from app.core.context import MediaInfo
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.helper.rss import RssHelper from app.helper.rss import RssHelper
from app.log import logger from app.log import logger
from app.schemas import MediaType from app.schemas import MediaType, Notification, MessageChannel, NotificationType
class DoubanChain(ChainBase): class DoubanChain(ChainBase):
@ -91,13 +91,15 @@ class DoubanChain(ChainBase):
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags, return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
page=page, count=count) page=page, count=count)
def remote_sync(self, userid: Union[int, str]): def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
""" """
同步豆瓣想看数据发送消息 同步豆瓣想看数据发送消息
""" """
self.post_message(title="开始同步豆瓣想看 ...", userid=userid) self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe,
title="开始同步豆瓣想看 ...", userid=userid))
self.sync() self.sync()
self.post_message(title="同步豆瓣想看数据完成!", userid=userid) self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe,
title="同步豆瓣想看数据完成!", userid=userid))
def sync(self): def sync(self):
""" """

View File

@ -8,8 +8,8 @@ from app.core.meta import MetaBase
from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.downloadhistory_oper import DownloadHistoryOper
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
from app.log import logger from app.log import logger
from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
from app.schemas.types import MediaType, TorrentStatus, EventType from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@ -23,7 +23,9 @@ class DownloadChain(ChainBase):
self.torrent = TorrentHelper() self.torrent = TorrentHelper()
self.downloadhis = DownloadHistoryOper() self.downloadhis = DownloadHistoryOper()
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, userid: str = None): def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo,
channel: MessageChannel = None,
userid: str = None):
""" """
发送添加下载的消息 发送添加下载的消息
""" """
@ -51,13 +53,17 @@ class DownloadChain(ChainBase):
torrent.description = re.sub(r'<[^>]+>', '', description) torrent.description = re.sub(r'<[^>]+>', '', description)
msg_text = f"{msg_text}\n描述:{torrent.description}" msg_text = f"{msg_text}\n描述:{torrent.description}"
self.post_message(title=f"{mediainfo.title_year}" self.post_message(Notification(
f"{meta.season_episode} 开始下载", channel=channel,
text=msg_text, mtype=NotificationType.Download,
image=mediainfo.get_message_image(), title=f"{mediainfo.title_year}"
userid=userid) f"{meta.season_episode} 开始下载",
text=msg_text,
image=mediainfo.get_message_image(),
userid=userid))
def download_torrent(self, torrent: TorrentInfo, def download_torrent(self, torrent: TorrentInfo,
channel: MessageChannel = None,
userid: Union[str, int] = None) -> Tuple[Optional[Path], str, list]: userid: Union[str, int] = None) -> Tuple[Optional[Path], str, list]:
""" """
下载种子文件 下载种子文件
@ -70,14 +76,19 @@ class DownloadChain(ChainBase):
proxy=torrent.site_proxy) proxy=torrent.site_proxy)
if not torrent_file: if not torrent_file:
logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}") logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}")
self.post_message(title=f"{torrent.title} 种子下载失败!", self.post_message(Notification(
text=f"错误信息:{error_msg}\n种子链接:{torrent.enclosure}", channel=channel,
userid=userid) mtype=NotificationType.Download,
title=f"{torrent.title} 种子下载失败!",
text=f"错误信息:{error_msg}\n种子链接:{torrent.enclosure}",
userid=userid))
return None, "", [] return None, "", []
return torrent_file, download_folder, files return torrent_file, download_folder, files
def download_single(self, context: Context, torrent_file: Path = None, def download_single(self, context: Context, torrent_file: Path = None,
episodes: Set[int] = None, userid: Union[str, int] = None) -> Optional[str]: episodes: Set[int] = None,
channel: MessageChannel = None,
userid: Union[str, int] = None) -> Optional[str]:
""" """
下载及发送通知 下载及发送通知
""" """
@ -119,7 +130,8 @@ class DownloadChain(ChainBase):
torrent_site=_torrent.site_name torrent_site=_torrent.site_name
) )
# 发送消息 # 发送消息
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent, userid=userid) self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent,
channel=channel, userid=userid)
# 下载成功后处理 # 下载成功后处理
self.download_added(context=context, torrent_path=torrent_file) self.download_added(context=context, torrent_path=torrent_file)
# 广播事件 # 广播事件
@ -132,7 +144,9 @@ class DownloadChain(ChainBase):
# 下载失败 # 下载失败
logger.error(f"{_media.title_year} 添加下载任务失败:" logger.error(f"{_media.title_year} 添加下载任务失败:"
f"{_torrent.title} - {_torrent.enclosure}{error_msg}") f"{_torrent.title} - {_torrent.enclosure}{error_msg}")
self.post_message( self.post_message(Notification(
channel=channel,
mtype=NotificationType.Download,
title="添加下载任务失败:%s %s" title="添加下载任务失败:%s %s"
% (_media.title_year, _meta.season_episode), % (_media.title_year, _meta.season_episode),
text=f"站点:{_torrent.site_name}\n" text=f"站点:{_torrent.site_name}\n"
@ -140,7 +154,7 @@ class DownloadChain(ChainBase):
f"种子链接:{_torrent.enclosure}\n" f"种子链接:{_torrent.enclosure}\n"
f"错误信息:{error_msg}", f"错误信息:{error_msg}",
image=_media.get_message_image(), image=_media.get_message_image(),
userid=userid) userid=userid))
return _hash return _hash
def batch_download(self, def batch_download(self,
@ -324,12 +338,12 @@ class DownloadChain(ChainBase):
if len(torrent_season) != 1 or torrent_season[0] != need_season: if len(torrent_season) != 1 or torrent_season[0] != need_season:
continue continue
# 种子集列表 # 种子集列表
torrent_episodes = meta.episode_list torrent_episodes = set(meta.episode_list)
# 整季的不处理 # 整季的不处理
if not torrent_episodes: if not torrent_episodes:
continue continue
# 为需要集的子集则下载 # 为需要集的子集则下载
if set(torrent_episodes).issubset(set(need_episodes)): if torrent_episodes.issubset(set(need_episodes)):
# 下载 # 下载
download_id = self.download_single(context, userid=userid) download_id = self.download_single(context, userid=userid)
if download_id: if download_id:
@ -517,13 +531,16 @@ class DownloadChain(ChainBase):
# 全部存在 # 全部存在
return True, no_exists return True, no_exists
def remote_downloading(self, userid: Union[str, int] = None): def remote_downloading(self, channel: MessageChannel, userid: Union[str, int] = None):
""" """
查询正在下载的任务并发送消息 查询正在下载的任务并发送消息
""" """
torrents = self.list_torrents(status=TorrentStatus.DOWNLOADING) torrents = self.list_torrents(status=TorrentStatus.DOWNLOADING)
if not torrents: if not torrents:
self.post_message(title="没有正在下载的任务!") self.post_message(Notification(
channel=channel,
mtype=NotificationType.Download,
title="没有正在下载的任务!"))
return return
# 发送消息 # 发送消息
title = f"{len(torrents)} 个任务正在下载:" title = f"{len(torrents)} 个任务正在下载:"
@ -534,7 +551,9 @@ class DownloadChain(ChainBase):
f"{StringUtils.str_filesize(torrent.size)} " f"{StringUtils.str_filesize(torrent.size)} "
f"{round(torrent.progress * 100, 1)}%") f"{round(torrent.progress * 100, 1)}%")
index += 1 index += 1
self.post_message(title=title, text="\n".join(messages), userid=userid) self.post_message(Notification(
channel=channel, mtype=NotificationType.Download,
title=title, text="\n".join(messages), userid=userid))
def downloading(self) -> List[DownloadingTorrent]: def downloading(self) -> List[DownloadingTorrent]:
""" """

View File

@ -7,7 +7,8 @@ from app.chain.subscribe import SubscribeChain
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import EventManager from app.core.event import EventManager
from app.log import logger from app.log import logger
from app.schemas.types import EventType from app.schemas import Notification
from app.schemas.types import EventType, MessageChannel
class MessageChain(ChainBase): class MessageChain(ChainBase):
@ -76,7 +77,7 @@ class MessageChain(ChainBase):
or not cache_data.get('items') \ or not cache_data.get('items') \
or len(cache_data.get('items')) < int(text): or len(cache_data.get('items')) < int(text):
# 发送消息 # 发送消息
self.post_message(title="输入有误!", userid=userid) self.post_message(Notification(channel=channel, title="输入有误!", userid=userid))
return return
# 缓存类型 # 缓存类型
cache_type: str = cache_data.get('type') cache_type: str = cache_data.get('type')
@ -90,9 +91,11 @@ class MessageChain(ChainBase):
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=self._current_meta, exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=self._current_meta,
mediainfo=self._current_media) mediainfo=self._current_media)
if exist_flag: if exist_flag:
self.post_message(title=f"{self._current_media.title_year}" self.post_message(
f"{self._current_meta.sea} 媒体库中已存在", Notification(channel=channel,
userid=userid) title=f"{self._current_media.title_year}"
f"{self._current_meta.sea} 媒体库中已存在",
userid=userid))
return return
# 发送缺失的媒体信息 # 发送缺失的媒体信息
if no_exists: if no_exists:
@ -100,19 +103,23 @@ class MessageChain(ChainBase):
messages = [ messages = [
f"{sea} 季缺失 {StringUtils.str_series(no_exist.episodes) if no_exist.episodes else no_exist.total_episodes}" f"{sea} 季缺失 {StringUtils.str_series(no_exist.episodes) if no_exist.episodes else no_exist.total_episodes}"
for sea, no_exist in no_exists.get(mediainfo.tmdb_id).items()] for sea, no_exist in no_exists.get(mediainfo.tmdb_id).items()]
self.post_message(title=f"{mediainfo.title_year}\n" + "\n".join(messages)) self.post_message(Notification(channel=channel,
title=f"{mediainfo.title_year}\n" + "\n".join(messages)))
# 搜索种子,过滤掉不需要的剧集,以便选择 # 搜索种子,过滤掉不需要的剧集,以便选择
logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...") logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...")
self.post_message( self.post_message(
title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", userid=userid) Notification(channel=channel,
title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...",
userid=userid))
# 开始搜索 # 开始搜索
contexts = self.searchchain.process(mediainfo=mediainfo, contexts = self.searchchain.process(mediainfo=mediainfo,
no_exists=no_exists) no_exists=no_exists)
if not contexts: if not contexts:
# 没有数据 # 没有数据
self.post_message(title=f"{mediainfo.title}" self.post_message(Notification(
f"{self._current_meta.sea} 未搜索到需要的资源!", channel=channel, title=f"{mediainfo.title}"
userid=userid) f"{self._current_meta.sea} 未搜索到需要的资源!",
userid=userid))
return return
# 搜索结果排序 # 搜索结果排序
contexts = self.torrenthelper.sort_torrents(contexts) contexts = self.torrenthelper.sort_torrents(contexts)
@ -124,9 +131,9 @@ class MessageChain(ChainBase):
self._current_page = 0 self._current_page = 0
# 发送种子数据 # 发送种子数据
logger.info(f"搜索到 {len(contexts)} 条数据,开始发送选择消息 ...") logger.info(f"搜索到 {len(contexts)} 条数据,开始发送选择消息 ...")
self.__post_torrents_message(title=mediainfo.title, self.__post_torrents_message(channel=channel,
title=mediainfo.title,
items=contexts[:self._page_size], items=contexts[:self._page_size],
mediainfo=mediainfo,
userid=userid, userid=userid,
total=len(contexts)) total=len(contexts))
@ -137,15 +144,18 @@ class MessageChain(ChainBase):
exist_flag, _ = self.downloadchain.get_no_exists_info(meta=self._current_meta, exist_flag, _ = self.downloadchain.get_no_exists_info(meta=self._current_meta,
mediainfo=mediainfo) mediainfo=mediainfo)
if exist_flag: if exist_flag:
self.post_message(title=f"{mediainfo.title_year}" self.post_message(Notification(
f"{self._current_meta.sea} 媒体库中已存在", channel=channel,
userid=userid) title=f"{mediainfo.title_year}"
f"{self._current_meta.sea} 媒体库中已存在",
userid=userid))
return return
self.subscribechain.add(title=mediainfo.title, self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year, year=mediainfo.year,
mtype=mediainfo.type, mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id, tmdbid=mediainfo.tmdb_id,
season=self._current_meta.begin_season, season=self._current_meta.begin_season,
channel=channel,
userid=userid, userid=userid,
username=username) username=username)
elif cache_type == "Torrent": elif cache_type == "Torrent":
@ -155,9 +165,11 @@ class MessageChain(ChainBase):
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=self._current_meta, exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=self._current_meta,
mediainfo=self._current_media) mediainfo=self._current_media)
if exist_flag: if exist_flag:
self.post_message(title=f"{self._current_media.title_year}" self.post_message(Notification(
f"{self._current_meta.sea} 媒体库中已存在", channel=channel,
userid=userid) title=f"{self._current_media.title_year}"
f"{self._current_meta.sea} 媒体库中已存在",
userid=userid))
return return
# 批量下载 # 批量下载
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list, downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
@ -175,6 +187,7 @@ class MessageChain(ChainBase):
mtype=self._current_media.type, mtype=self._current_media.type,
tmdbid=self._current_media.tmdb_id, tmdbid=self._current_media.tmdb_id,
season=self._current_meta.begin_season, season=self._current_meta.begin_season,
channel=channel,
userid=userid, userid=userid,
username=username) username=username)
else: else:
@ -188,12 +201,14 @@ class MessageChain(ChainBase):
cache_data: dict = self._user_cache.get(userid) cache_data: dict = self._user_cache.get(userid)
if not cache_data: if not cache_data:
# 没有缓存 # 没有缓存
self.post_message(title="输入有误!", userid=userid) self.post_message(Notification(
channel=channel, title="输入有误!", userid=userid))
return return
if self._current_page == 0: if self._current_page == 0:
# 第一页 # 第一页
self.post_message(title="已经是第一页了!", userid=userid) self.post_message(Notification(
channel=channel, title="已经是第一页了!", userid=userid))
return return
cache_type: str = cache_data.get('type') cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items') cache_list: list = cache_data.get('items')
@ -207,14 +222,15 @@ class MessageChain(ChainBase):
end = start + self._page_size end = start + self._page_size
if cache_type == "Torrent": if cache_type == "Torrent":
# 发送种子数据 # 发送种子数据
self.__post_torrents_message(title=self._current_media.title, self.__post_torrents_message(channel=channel,
title=self._current_media.title,
items=cache_list[start:end], items=cache_list[start:end],
mediainfo=self._current_media,
userid=userid, userid=userid,
total=len(cache_list)) total=len(cache_list))
else: else:
# 发送媒体数据 # 发送媒体数据
self.__post_medias_message(title=self._current_media.title, self.__post_medias_message(channel=channel,
title=self._current_media.title,
items=cache_list[start:end], items=cache_list[start:end],
userid=userid, userid=userid,
total=len(cache_list)) total=len(cache_list))
@ -224,7 +240,8 @@ class MessageChain(ChainBase):
cache_data: dict = self._user_cache.get(userid) cache_data: dict = self._user_cache.get(userid)
if not cache_data: if not cache_data:
# 没有缓存 # 没有缓存
self.post_message(title="输入有误!", userid=userid) self.post_message(Notification(
channel=channel, title="输入有误!", userid=userid))
return return
cache_type: str = cache_data.get('type') cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items') cache_list: list = cache_data.get('items')
@ -234,17 +251,19 @@ class MessageChain(ChainBase):
cache_list = cache_list[self._current_page * self._page_size:(self._current_page + 1) * self._page_size] cache_list = cache_list[self._current_page * self._page_size:(self._current_page + 1) * self._page_size]
if not cache_list: if not cache_list:
# 没有数据 # 没有数据
self.post_message(title="已经是最后一页了!", userid=userid) self.post_message(Notification(
channel=channel, title="已经是最后一页了!", userid=userid))
return return
else: else:
if cache_type == "Torrent": if cache_type == "Torrent":
# 发送种子数据 # 发送种子数据
self.__post_torrents_message(title=self._current_media.title, self.__post_torrents_message(channel=channel,
mediainfo=self._current_media, title=self._current_media.title,
items=cache_list, userid=userid, total=total) items=cache_list, userid=userid, total=total)
else: else:
# 发送媒体数据 # 发送媒体数据
self.__post_medias_message(title=self._current_media.title, self.__post_medias_message(channel=channel,
title=self._current_media.title,
items=cache_list, userid=userid, total=total) items=cache_list, userid=userid, total=total)
else: else:
@ -261,11 +280,13 @@ class MessageChain(ChainBase):
meta, medias = self.medtachain.search(content) meta, medias = self.medtachain.search(content)
# 识别 # 识别
if not meta.name: if not meta.name:
self.post_message(title="无法识别输入内容!", userid=userid) self.post_message(Notification(
channel=channel, title="无法识别输入内容!", userid=userid))
return return
# 开始搜索 # 开始搜索
if not medias: if not medias:
self.post_message(title=f"{meta.name} 没有找到对应的媒体信息!", userid=userid) self.post_message(Notification(
channel=channel, title=f"{meta.name} 没有找到对应的媒体信息!", userid=userid))
return return
logger.info(f"搜索到 {len(medias)} 条相关媒体信息") logger.info(f"搜索到 {len(medias)} 条相关媒体信息")
# 记录当前状态 # 记录当前状态
@ -277,28 +298,30 @@ class MessageChain(ChainBase):
self._current_page = 0 self._current_page = 0
self._current_media = None self._current_media = None
# 发送媒体列表 # 发送媒体列表
self.__post_medias_message(title=meta.name, self.__post_medias_message(channel=channel,
title=meta.name,
items=medias[:self._page_size], items=medias[:self._page_size],
userid=userid, total=len(medias)) userid=userid, total=len(medias))
def __post_medias_message(self, title: str, items: list, userid: str, total: int): def __post_medias_message(self, channel: MessageChannel,
title: str, items: list, userid: str, total: int):
""" """
发送媒体列表消息 发送媒体列表消息
""" """
self.post_medias_message( self.post_medias_message(Notification(
channel=channel,
title=f"{title}】共找到{total}条相关信息请回复对应数字选择p: 上一页 n: 下一页)", title=f"{title}】共找到{total}条相关信息请回复对应数字选择p: 上一页 n: 下一页)",
items=items,
userid=userid userid=userid
) ), medias=items)
def __post_torrents_message(self, title: str, items: list, def __post_torrents_message(self, channel: MessageChannel, title: str, items: list,
mediainfo: MediaInfo, userid: str, total: int): userid: str, total: int):
""" """
发送种子列表消息 发送种子列表消息
""" """
self.post_torrents_message( self.post_torrents_message(Notification(
channel=channel,
title=f"{title}】共找到{total}条相关资源请回复对应数字下载0: 自动选择 p: 上一页 n: 下一页)", title=f"{title}】共找到{total}条相关资源请回复对应数字下载0: 自动选择 p: 上一页 n: 下一页)",
items=items, items=items,
mediainfo=mediainfo,
userid=userid userid=userid
) ), torrents=items)

View File

@ -16,7 +16,6 @@ from app.helper.torrent import TorrentHelper
from app.log import logger from app.log import logger
from app.schemas import NotExistMediaInfo from app.schemas import NotExistMediaInfo
from app.schemas.types import MediaType, ProgressKey, SystemConfigKey from app.schemas.types import MediaType, ProgressKey, SystemConfigKey
from app.utils.object import ObjectUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -9,6 +9,7 @@ from app.helper.cloudflare import under_challenge
from app.helper.cookie import CookieHelper from app.helper.cookie import CookieHelper
from app.helper.message import MessageHelper from app.helper.message import MessageHelper
from app.log import logger from app.log import logger
from app.schemas import MessageChannel, Notification
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.site import SiteUtils from app.utils.site import SiteUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils
@ -80,13 +81,15 @@ class SiteChain(ChainBase):
return False, f"{str(e)}" return False, f"{str(e)}"
return True, "连接成功" return True, "连接成功"
def remote_list(self, userid: Union[str, int] = None): def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
""" """
查询所有站点发送消息 查询所有站点发送消息
""" """
site_list = self.siteoper.list() site_list = self.siteoper.list()
if not site_list: if not site_list:
self.post_message(title="没有维护任何站点信息!") self.post_message(Notification(
channel=channel,
title="没有维护任何站点信息!"))
title = f"共有 {len(site_list)} 个站点,回复对应指令操作:" \ title = f"共有 {len(site_list)} 个站点,回复对应指令操作:" \
f"\n- 禁用站点:/site_disable [id]" \ f"\n- 禁用站点:/site_disable [id]" \
f"\n- 启用站点:/site_enable [id]" \ f"\n- 启用站点:/site_enable [id]" \
@ -102,9 +105,11 @@ class SiteChain(ChainBase):
else: else:
messages.append(f"{site.id}. {site.name}") messages.append(f"{site.id}. {site.name}")
# 发送列表 # 发送列表
self.post_message(title=title, text="\n".join(messages), userid=userid) self.post_message(Notification(
channel=channel,
title=title, text="\n".join(messages), userid=userid))
def remote_disable(self, arg_str, userid: Union[str, int] = None): def remote_disable(self, arg_str, channel: MessageChannel, userid: Union[str, int] = None):
""" """
禁用站点 禁用站点
""" """
@ -116,16 +121,18 @@ class SiteChain(ChainBase):
site_id = int(arg_str) site_id = int(arg_str)
site = self.siteoper.get(site_id) site = self.siteoper.get(site_id)
if not site: if not site:
self.post_message(title=f"站点编号 {site_id} 不存在!", userid=userid) self.post_message(Notification(
channel=channel,
title=f"站点编号 {site_id} 不存在!", userid=userid))
return return
# 禁用站点 # 禁用站点
self.siteoper.update(site_id, { self.siteoper.update(site_id, {
"is_active": False "is_active": False
}) })
# 重新发送消息 # 重新发送消息
self.remote_list() self.remote_list(channel, userid)
def remote_enable(self, arg_str, userid: Union[str, int] = None): def remote_enable(self, arg_str, channel: MessageChannel, userid: Union[str, int] = None):
""" """
启用站点 启用站点
""" """
@ -139,14 +146,16 @@ class SiteChain(ChainBase):
site_id = int(arg_str) site_id = int(arg_str)
site = self.siteoper.get(site_id) site = self.siteoper.get(site_id)
if not site: if not site:
self.post_message(title=f"站点编号 {site_id} 不存在!", userid=userid) self.post_message(Notification(
channel=channel,
title=f"站点编号 {site_id} 不存在!", userid=userid))
return return
# 禁用站点 # 禁用站点
self.siteoper.update(site_id, { self.siteoper.update(site_id, {
"is_active": True "is_active": True
}) })
# 重新发送消息 # 重新发送消息
self.remote_list() self.remote_list(channel, userid)
def update_cookie(self, site_info: Site, def update_cookie(self, site_info: Site,
username: str, password: str) -> Tuple[bool, str]: username: str, password: str) -> Tuple[bool, str]:
@ -175,32 +184,42 @@ class SiteChain(ChainBase):
return True, msg return True, msg
return False, "未知错误" return False, "未知错误"
def remote_cookie(self, arg_str: str, userid: Union[str, int] = None): def remote_cookie(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None):
""" """
使用用户名密码更新站点Cookie 使用用户名密码更新站点Cookie
""" """
err_title = "请输入正确的命令格式:/site_cookie [id] [username] [password]" \ err_title = "请输入正确的命令格式:/site_cookie [id] [username] [password]" \
"[id]为站点编号,[uername]为站点用户名,[password]为站点密码" "[id]为站点编号,[uername]为站点用户名,[password]为站点密码"
if not arg_str: if not arg_str:
self.post_message(title=err_title, userid=userid) self.post_message(Notification(
channel=channel,
title=err_title, userid=userid))
return return
arg_str = str(arg_str).strip() arg_str = str(arg_str).strip()
args = arg_str.split() args = arg_str.split()
if len(args) != 3: if len(args) != 3:
self.post_message(title=err_title, userid=userid) self.post_message(Notification(
channel=channel,
title=err_title, userid=userid))
return return
site_id = args[0] site_id = args[0]
if not site_id.isdigit(): if not site_id.isdigit():
self.post_message(title=err_title, userid=userid) self.post_message(Notification(
channel=channel,
title=err_title, userid=userid))
return return
# 站点ID # 站点ID
site_id = int(site_id) site_id = int(site_id)
# 站点信息 # 站点信息
site_info = self.siteoper.get(site_id) site_info = self.siteoper.get(site_id)
if not site_info: if not site_info:
self.post_message(title=f"站点编号 {site_id} 不存在!", userid=userid) self.post_message(Notification(
channel=channel,
title=f"站点编号 {site_id} 不存在!", userid=userid))
return return
self.post_message(title=f"开始更新【{site_info.name}】Cookie&UA ...", userid=userid) self.post_message(Notification(
channel=channel,
title=f"开始更新【{site_info.name}】Cookie&UA ...", userid=userid))
# 用户名 # 用户名
username = args[1] username = args[1]
# 密码 # 密码
@ -211,9 +230,13 @@ class SiteChain(ChainBase):
password=password) password=password)
if not status: if not status:
logger.error(msg) logger.error(msg)
self.post_message(title=f"{site_info.name}】 Cookie&UA更新失败", self.post_message(Notification(
text=f"错误原因:{msg}", channel=channel,
userid=userid) title=f"{site_info.name}】 Cookie&UA更新失败",
text=f"错误原因:{msg}",
userid=userid))
else: else:
self.post_message(title=f"{site_info.name}】 Cookie&UA更新成功", self.post_message(Notification(
userid=userid) channel=channel,
title=f"{site_info.name}】 Cookie&UA更新成功",
userid=userid))

View File

@ -6,18 +6,17 @@ from typing import Dict, List, Optional, Union, Tuple
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.core.metainfo import MetaInfo
from app.core.context import TorrentInfo, Context, MediaInfo from app.core.context import TorrentInfo, Context, MediaInfo
from app.core.config import settings from app.core.metainfo import MetaInfo
from app.db.models.subscribe import Subscribe from app.db.models.subscribe import Subscribe
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.helper.message import MessageHelper from app.helper.message import MessageHelper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.log import logger from app.log import logger
from app.schemas import NotExistMediaInfo from app.schemas import NotExistMediaInfo, Notification
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.schemas.types import MediaType, SystemConfigKey
class SubscribeChain(ChainBase): class SubscribeChain(ChainBase):
@ -42,6 +41,7 @@ class SubscribeChain(ChainBase):
tmdbid: int = None, tmdbid: int = None,
doubanid: str = None, doubanid: str = None,
season: int = None, season: int = None,
channel: MessageChannel = None,
userid: str = None, userid: str = None,
username: str = None, username: str = None,
message: bool = True, message: bool = True,
@ -106,11 +106,13 @@ class SubscribeChain(ChainBase):
logger.error(f'{mediainfo.title_year} {err_msg}') logger.error(f'{mediainfo.title_year} {err_msg}')
if not exist_ok and message: if not exist_ok and message:
# 发回原用户 # 发回原用户
self.post_message(title=f"{mediainfo.title_year}{metainfo.season} " self.post_message(Notification(channel=channel,
f"添加订阅失败!", mtype=NotificationType.Subscribe,
text=f"{err_msg}", title=f"{mediainfo.title_year}{metainfo.season} "
image=mediainfo.get_message_image(), f"添加订阅失败!",
userid=userid) text=f"{err_msg}",
image=mediainfo.get_message_image(),
userid=userid))
elif message: elif message:
logger.info(f'{mediainfo.title_year}{metainfo.season} 添加订阅成功') logger.info(f'{mediainfo.title_year}{metainfo.season} 添加订阅成功')
if username or userid: if username or userid:
@ -118,42 +120,52 @@ class SubscribeChain(ChainBase):
else: else:
text = f"评分:{mediainfo.vote_average}" text = f"评分:{mediainfo.vote_average}"
# 广而告之 # 广而告之
self.post_message(title=f"{mediainfo.title_year}{metainfo.season} 已添加订阅", self.post_message(Notification(channel=channel,
text=text, mtype=NotificationType.Subscribe,
image=mediainfo.get_message_image()) title=f"{mediainfo.title_year}{metainfo.season} 已添加订阅",
text=text,
image=mediainfo.get_message_image()))
# 返回结果 # 返回结果
return sid, "" return sid, ""
def remote_refresh(self, userid: Union[str, int] = None): def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
""" """
远程刷新订阅发送消息 远程刷新订阅发送消息
""" """
self.post_message(title=f"开始刷新订阅 ...", userid=userid) self.post_message(Notification(channel=channel,
title=f"开始刷新订阅 ...", userid=userid))
self.refresh() self.refresh()
self.post_message(title=f"订阅刷新完成!", userid=userid) self.post_message(Notification(channel=channel,
title=f"订阅刷新完成!", userid=userid))
def remote_search(self, arg_str: str, userid: Union[str, int] = None): def remote_search(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None):
""" """
远程搜索订阅发送消息 远程搜索订阅发送消息
""" """
if arg_str and not str(arg_str).isdigit(): if arg_str and not str(arg_str).isdigit():
self.post_message(title="请输入正确的命令格式:/subscribe_search [id]" self.post_message(Notification(channel=channel,
"[id]为订阅编号,不输入订阅编号时搜索所有订阅", userid=userid) title="请输入正确的命令格式:/subscribe_search [id]"
"[id]为订阅编号,不输入订阅编号时搜索所有订阅", userid=userid))
return return
if arg_str: if arg_str:
sid = int(arg_str) sid = int(arg_str)
subscribe = self.subscribehelper.get(sid) subscribe = self.subscribehelper.get(sid)
if not subscribe: if not subscribe:
self.post_message(title=f"订阅编号 {sid} 不存在!", userid=userid) self.post_message(Notification(channel=channel,
title=f"订阅编号 {sid} 不存在!", userid=userid))
return return
self.post_message(title=f"开始搜索 {subscribe.name} ...", userid=userid) self.post_message(Notification(channel=channel,
title=f"开始搜索 {subscribe.name} ...", userid=userid))
# 搜索订阅 # 搜索订阅
self.search(sid=int(arg_str)) self.search(sid=int(arg_str))
self.post_message(title=f"{subscribe.name} 搜索完成!", userid=userid) self.post_message(Notification(channel=channel,
title=f"{subscribe.name} 搜索完成!", userid=userid))
else: else:
self.post_message(title=f"开始搜索所有订阅 ...", userid=userid) self.post_message(Notification(channel=channel,
title=f"开始搜索所有订阅 ...", userid=userid))
self.search(state='R') self.search(state='R')
self.post_message(title=f"订阅搜索完成!", userid=userid) self.post_message(Notification(channel=channel,
title=f"订阅搜索完成!", userid=userid))
def search(self, sid: int = None, state: str = 'N', manual: bool = False): def search(self, sid: int = None, state: str = 'N', manual: bool = False):
""" """
@ -189,8 +201,9 @@ class SubscribeChain(ChainBase):
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅') logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribehelper.delete(subscribe.id) self.subscribehelper.delete(subscribe.id)
# 发送通知 # 发送通知
self.post_message(title=f'{mediainfo.title_year}{meta.season} 已完成订阅', self.post_message(Notification(mtype=NotificationType.Subscribe,
image=mediainfo.get_message_image()) title=f'{mediainfo.title_year}{meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue continue
# 使用订阅的总集数和开始集数替换no_exists # 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits( no_exists = self.__get_subscribe_no_exits(
@ -255,8 +268,9 @@ class SubscribeChain(ChainBase):
logger.info(f'{mediainfo.title_year} 下载完成,完成订阅') logger.info(f'{mediainfo.title_year} 下载完成,完成订阅')
self.subscribehelper.delete(subscribe.id) self.subscribehelper.delete(subscribe.id)
# 发送通知 # 发送通知
self.post_message(title=f'{mediainfo.title_year}{meta.season} 已完成订阅', self.post_message(Notification(mtype=NotificationType.Subscribe,
image=mediainfo.get_message_image()) title=f'{mediainfo.title_year}{meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
else: else:
# 未完成下载 # 未完成下载
logger.info(f'{mediainfo.title_year} 未下载未完整,继续订阅 ...') logger.info(f'{mediainfo.title_year} 未下载未完整,继续订阅 ...')
@ -343,8 +357,9 @@ class SubscribeChain(ChainBase):
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅') logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribehelper.delete(subscribe.id) self.subscribehelper.delete(subscribe.id)
# 发送通知 # 发送通知
self.post_message(title=f'{mediainfo.title_year}{meta.season} 已完成订阅', self.post_message(Notification(mtype=NotificationType.Subscribe,
image=mediainfo.get_message_image()) title=f'{mediainfo.title_year}{meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue continue
# 使用订阅的总集数和开始集数替换no_exists # 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits( no_exists = self.__get_subscribe_no_exits(
@ -404,8 +419,9 @@ class SubscribeChain(ChainBase):
logger.info(f'{mediainfo.title_year} 下载完成,完成订阅') logger.info(f'{mediainfo.title_year} 下载完成,完成订阅')
self.subscribehelper.delete(subscribe.id) self.subscribehelper.delete(subscribe.id)
# 发送通知 # 发送通知
self.post_message(title=f'{mediainfo.title_year}{meta.season} 已完成订阅', self.post_message(Notification(mtype=NotificationType.Subscribe,
image=mediainfo.get_message_image()) title=f'{mediainfo.title_year}{meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
else: else:
update_date = True if downloads else False update_date = True if downloads else False
# 未完成下载,计算剩余集数 # 未完成下载,计算剩余集数
@ -481,13 +497,14 @@ class SubscribeChain(ChainBase):
"lack_episode": len(left_episodes) "lack_episode": len(left_episodes)
}) })
def remote_list(self, userid: Union[str, int] = None): def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
""" """
查询订阅并发送消息 查询订阅并发送消息
""" """
subscribes = self.subscribehelper.list() subscribes = self.subscribehelper.list()
if not subscribes: if not subscribes:
self.post_message(title='没有任何订阅!', userid=userid) self.post_message(Notification(channel=channel,
title='没有任何订阅!', userid=userid))
return return
title = f"共有 {len(subscribes)} 个订阅,回复对应指令操作: " \ title = f"共有 {len(subscribes)} 个订阅,回复对应指令操作: " \
f"\n- 删除订阅:/subscribe_delete [id]" \ f"\n- 删除订阅:/subscribe_delete [id]" \
@ -505,15 +522,17 @@ class SubscribeChain(ChainBase):
f"_{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}" f"_{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}"
f"/{subscribe.total_episode}_") f"/{subscribe.total_episode}_")
# 发送列表 # 发送列表
self.post_message(title=title, text='\n'.join(messages), userid=userid) self.post_message(Notification(channel=channel,
title=title, text='\n'.join(messages), userid=userid))
def remote_delete(self, arg_str: str, userid: Union[str, int] = None): def remote_delete(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None):
""" """
删除订阅 删除订阅
""" """
if not arg_str: if not arg_str:
self.post_message(title="请输入正确的命令格式:/subscribe_delete [id]" self.post_message(Notification(channel=channel,
"[id]为订阅编号", userid=userid) title="请输入正确的命令格式:/subscribe_delete [id]"
"[id]为订阅编号", userid=userid))
return return
arg_strs = str(arg_str).split() arg_strs = str(arg_str).split()
for arg_str in arg_strs: for arg_str in arg_strs:
@ -523,12 +542,13 @@ class SubscribeChain(ChainBase):
subscribe_id = int(arg_str) subscribe_id = int(arg_str)
subscribe = self.subscribehelper.get(subscribe_id) subscribe = self.subscribehelper.get(subscribe_id)
if not subscribe: if not subscribe:
self.post_message(title=f"订阅编号 {subscribe_id} 不存在!", userid=userid) self.post_message(Notification(channel=channel,
title=f"订阅编号 {subscribe_id} 不存在!", userid=userid))
return return
# 删除订阅 # 删除订阅
self.subscribehelper.delete(subscribe_id) self.subscribehelper.delete(subscribe_id)
# 重新发送消息 # 重新发送消息
self.remote_list() self.remote_list(channel, userid)
@staticmethod @staticmethod
def __get_subscribe_no_exits(no_exists: Dict[int, Dict[int, NotExistMediaInfo]], def __get_subscribe_no_exits(no_exists: Dict[int, Dict[int, NotExistMediaInfo]],

View File

@ -13,8 +13,8 @@ from app.db.models.downloadhistory import DownloadHistory
from app.db.transferhistory_oper import TransferHistoryOper from app.db.transferhistory_oper import TransferHistoryOper
from app.helper.progress import ProgressHelper from app.helper.progress import ProgressHelper
from app.log import logger from app.log import logger
from app.schemas import TransferInfo, TransferTorrent from app.schemas import TransferInfo, TransferTorrent, Notification
from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey, MessageChannel, NotificationType
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
@ -30,10 +30,11 @@ class TransferChain(ChainBase):
self.transferhis = TransferHistoryOper() self.transferhis = TransferHistoryOper()
self.progress = ProgressHelper() self.progress = ProgressHelper()
def process(self, arg_str: str = None, userid: Union[str, int] = None) -> bool: def process(self, arg_str: str = None, channel: MessageChannel = None, userid: Union[str, int] = None) -> bool:
""" """
获取下载器中的种子列表并执行转移 获取下载器中的种子列表并执行转移
:param arg_str: 传入的参数 (种子hash和TMDB ID) :param arg_str: 传入的参数 (种子hash和TMDB ID)
:param channel: 消息通道
:param userid: 用户ID :param userid: 用户ID
""" """
@ -110,9 +111,12 @@ class TransferChain(ChainBase):
mediainfo: MediaInfo = self.recognize_media(meta=meta) mediainfo: MediaInfo = self.recognize_media(meta=meta)
if not mediainfo: if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{torrent.title}') logger.warn(f'未识别到媒体信息,标题:{torrent.title}')
self.post_message(title=f"{torrent.title} 未识别到媒体信息,无法入库!\n" self.post_message(Notification(
f"回复:```\n/transfer {torrent.hash} [tmdbid]\n``` 手动识别转移。", channel=channel,
userid=userid) mtype=NotificationType.Organize,
title=f"{torrent.title} 未识别到媒体信息,无法入库!\n"
f"回复:```\n/transfer {torrent.hash} [tmdbid]\n``` 手动识别转移。",
userid=userid))
# 新增转移失败历史记录 # 新增转移失败历史记录
self.transferhis.add( self.transferhis.add(
src=str(torrent.path), src=str(torrent.path),
@ -134,12 +138,13 @@ class TransferChain(ChainBase):
if not transferinfo or not transferinfo.target_path: if not transferinfo or not transferinfo.target_path:
# 转移失败 # 转移失败
logger.warn(f"{torrent.title} 入库失败") logger.warn(f"{torrent.title} 入库失败")
self.post_message( self.post_message(Notification(
channel=channel,
title=f"{mediainfo.title_year}{meta.season_episode} 入库失败!", title=f"{mediainfo.title_year}{meta.season_episode} 入库失败!",
text=f"原因:{transferinfo.message if transferinfo else '未知'}", text=f"原因:{transferinfo.message if transferinfo else '未知'}",
image=mediainfo.get_message_image(), image=mediainfo.get_message_image(),
userid=userid userid=userid
) ))
# 新增转移失败历史记录 # 新增转移失败历史记录
self.transferhis.add( self.transferhis.add(
src=str(torrent.path), src=str(torrent.path),
@ -223,7 +228,9 @@ class TransferChain(ChainBase):
if transferinfo.message: if transferinfo.message:
msg_str = f"{msg_str},以下文件处理失败:\n{transferinfo.message}" msg_str = f"{msg_str},以下文件处理失败:\n{transferinfo.message}"
# 发送 # 发送
self.post_message(title=msg_title, text=msg_str, image=mediainfo.get_message_image()) self.post_message(Notification(
mtype=NotificationType.Organize,
title=msg_title, text=msg_str, image=mediainfo.get_message_image()))
@staticmethod @staticmethod
def delete_files(path: Path): def delete_files(path: Path):

View File

@ -2,8 +2,9 @@ import time
from typing import Any from typing import Any
from app.chain import ChainBase from app.chain import ChainBase
from app.schemas import Notification
from app.utils.http import WebUtils from app.utils.http import WebUtils
from app.schemas.types import EventType, MediaImageType, MediaType from app.schemas.types import EventType, MediaImageType, MediaType, NotificationType
class WebhookChain(ChainBase): class WebhookChain(ChainBase):
@ -92,4 +93,5 @@ class WebhookChain(ChainBase):
image_url = _webhook_images.get(event_info.get("channel")) image_url = _webhook_images.get(event_info.get("channel"))
# 发送消息 # 发送消息
self.post_message(title=message_title, text=message_content, image=image_url) self.post_message(Notification(mtype=NotificationType.MediaServer,
title=message_title, text=message_content, image=image_url))

View File

@ -15,7 +15,7 @@ from app.core.event import Event as ManagerEvent
from app.log import logger from app.log import logger
from app.utils.object import ObjectUtils from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
from app.schemas.types import EventType from app.schemas.types import EventType, MessageChannel
class CommandChian(ChainBase): class CommandChian(ChainBase):
@ -173,7 +173,8 @@ class Command(metaclass=Singleton):
""" """
return self._commands.get(cmd, {}) return self._commands.get(cmd, {})
def execute(self, cmd: str, data_str: str = "", userid: Union[str, int] = None) -> None: def execute(self, cmd: str, data_str: str = "",
channel: MessageChannel = None, userid: Union[str, int] = None) -> None:
""" """
执行命令 执行命令
""" """
@ -187,12 +188,12 @@ class Command(metaclass=Singleton):
if cmd_data: if cmd_data:
# 有内置参数直接使用内置参数 # 有内置参数直接使用内置参数
command['func'](**cmd_data) command['func'](**cmd_data)
elif args_num == 1: elif args_num == 2:
# 没有用户输入参数 # 没有输入参数只输入渠道和用户ID
command['func'](userid) command['func'](channel, userid)
else: else:
# 多个输入参数用户输入、用户ID # 多个输入参数用户输入、用户ID
command['func'](data_str, userid) command['func'](data_str, channel, userid)
else: else:
# 没有参数 # 没有参数
command['func']() command['func']()

View File

@ -1,6 +1,10 @@
from abc import abstractmethod, ABCMeta from abc import abstractmethod, ABCMeta
from typing import Tuple, Union from typing import Tuple, Union
from app.db.systemconfig_oper import SystemConfigOper
from app.schemas import Notification
from app.schemas.types import SystemConfigKey, MessageChannel
class _ModuleBase(metaclass=ABCMeta): class _ModuleBase(metaclass=ABCMeta):
""" """
@ -30,3 +34,32 @@ class _ModuleBase(metaclass=ABCMeta):
:return: None该方法可被多个模块同时处理 :return: None该方法可被多个模块同时处理
""" """
pass pass
def checkMessage(channel_type: MessageChannel):
"""
检查消息渠道及消息类型如不符合则不处理
"""
def decorator(func):
def wrapper(self, message: Notification, *args, **kwargs):
# 检查消息渠道
if message.channel and message.channel != channel_type:
return None
else:
# 检查消息类型开关
if message.mtype:
switchs = SystemConfigOper().get(SystemConfigKey.NotificationChannels)
for switch in switchs:
if switch.get("mtype") == message.mtype.value:
if channel_type == MessageChannel.Wechat and not switch.get("wechat"):
return None
if channel_type == MessageChannel.Telegram and not switch.get("telegram"):
return None
if channel_type == MessageChannel.Slack and not switch.get("slack"):
return None
return func(self, message, *args, **kwargs)
return wrapper
return decorator

View File

@ -5,9 +5,9 @@ from typing import Optional, Union, List, Tuple, Any
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase, checkMessage
from app.modules.slack.slack import Slack from app.modules.slack.slack import Slack
from app.schemas import MessageChannel, CommingMessage from app.schemas import MessageChannel, CommingMessage, Notification
class SlackModule(_ModuleBase): class SlackModule(_ModuleBase):
@ -181,40 +181,33 @@ class SlackModule(_ModuleBase):
userid=userid, username=username, text=text) userid=userid, username=username, text=text)
return None return None
def post_message(self, title: str, @checkMessage(MessageChannel.Slack)
text: str = None, image: str = None, def post_message(self, message: Notification) -> Optional[bool]:
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送消息 发送消息
:param title: 标题 :param message: 消息
:param text: 内容
:param image: 图片
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.slack.send_msg(title=title, text=text, image=image, userid=userid) return self.slack.send_msg(title=message.title, text=message.text,
image=message.image, userid=message.userid)
def post_medias_message(self, title: str, items: List[MediaInfo], @checkMessage(MessageChannel.Slack)
userid: Union[str, int] = None) -> Optional[bool]: def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
""" """
发送媒体信息选择列表 发送媒体信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param medias: 媒体信息
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.slack.send_meidas_msg(title=title, medias=items, userid=userid) return self.slack.send_meidas_msg(title=message.title, medias=medias, userid=message.userid)
def post_torrents_message(self, title: str, items: List[Context], @checkMessage(MessageChannel.Slack)
mediainfo: MediaInfo = None, def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送种子信息选择列表 发送种子信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param torrents: 种子信息
:param mediainfo: 媒体信息
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.slack.send_torrents_msg(title=title, torrents=items, return self.slack.send_torrents_msg(title=message.title, torrents=torrents,
userid=userid) userid=message.userid)

View File

@ -4,9 +4,9 @@ from typing import Optional, Union, List, Tuple, Any
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase, checkMessage
from app.modules.telegram.telegram import Telegram from app.modules.telegram.telegram import Telegram
from app.schemas import MessageChannel, CommingMessage from app.schemas import MessageChannel, CommingMessage, Notification
class TelegramModule(_ModuleBase): class TelegramModule(_ModuleBase):
@ -91,42 +91,36 @@ class TelegramModule(_ModuleBase):
userid=user_id, username=user_id, text=text) userid=user_id, username=user_id, text=text)
return None return None
def post_message(self, title: str, @checkMessage(MessageChannel.Telegram)
text: str = None, image: str = None, userid: Union[str, int] = None) -> Optional[bool]: def post_message(self, message: Notification) -> Optional[bool]:
""" """
发送消息 发送消息
:param title: 标题 :param message: 消息体
:param text: 内容
:param image: 图片
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.telegram.send_msg(title=title, text=text, image=image, userid=userid) return self.telegram.send_msg(title=message.title, text=message.text,
image=message.image, userid=message.userid)
def post_medias_message(self, title: str, items: List[MediaInfo], @checkMessage(MessageChannel.Telegram)
userid: Union[str, int] = None) -> Optional[bool]: def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
""" """
发送媒体信息选择列表 发送媒体信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param medias: 媒体列表
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.telegram.send_meidas_msg(title=title, medias=items, userid=userid) return self.telegram.send_meidas_msg(title=message.title, medias=medias,
userid=message.userid)
def post_torrents_message(self, title: str, items: List[Context], @checkMessage(MessageChannel.Telegram)
mediainfo: MediaInfo = None, def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送种子信息选择列表 发送种子信息选择列表
:param title: 标题 :param message: 消息体
:param items: 消息列表 :param torrents: 种子列表
:param mediainfo: 媒体信息
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.telegram.send_torrents_msg(title=title, torrents=items, return self.telegram.send_torrents_msg(title=message.title, torrents=torrents, userid=message.userid)
mediainfo=mediainfo, userid=userid)
def register_commands(self, commands: dict): def register_commands(self, commands: dict):
""" """

View File

@ -133,7 +133,6 @@ class Telegram(metaclass=Singleton):
return False return False
def send_torrents_msg(self, torrents: List[Context], def send_torrents_msg(self, torrents: List[Context],
mediainfo: MediaInfo = None,
userid: str = "", title: str = "") -> Optional[bool]: userid: str = "", title: str = "") -> Optional[bool]:
""" """
发送列表消息 发送列表消息
@ -141,8 +140,12 @@ class Telegram(metaclass=Singleton):
if not self._telegram_token or not self._telegram_chat_id: if not self._telegram_token or not self._telegram_chat_id:
return None return None
if not torrents:
return False
try: try:
index, caption = 1, "*%s*" % title index, caption = 1, "*%s*" % title
mediainfo = torrents[0].media_info
for context in torrents: for context in torrents:
torrent = context.torrent_info torrent = context.torrent_info
site_name = torrent.site_name site_name = torrent.site_name

View File

@ -1,13 +1,13 @@
import xml.dom.minidom import xml.dom.minidom
from typing import Optional, Union, List, Tuple, Any from typing import Optional, Union, List, Tuple, Any
from app.core.context import MediaInfo, Context
from app.core.config import settings from app.core.config import settings
from app.core.context import Context, MediaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase, checkMessage
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
from app.modules.wechat.wechat import WeChat from app.modules.wechat.wechat import WeChat
from app.schemas import MessageChannel, CommingMessage from app.schemas import MessageChannel, CommingMessage, Notification
from app.utils.dom import DomUtils from app.utils.dom import DomUtils
@ -114,41 +114,35 @@ class WechatModule(_ModuleBase):
logger.error(f"微信消息处理发生错误:{err}") logger.error(f"微信消息处理发生错误:{err}")
return None return None
def post_message(self, title: str, @checkMessage(MessageChannel.Wechat)
text: str = None, image: str = None, userid: Union[str, int] = None) -> Optional[bool]: def post_message(self, message: Notification) -> Optional[bool]:
""" """
发送消息 发送消息
:param title: 标题 :param message: 消息内容
:param text: 内容
:param image: 图片
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.wechat.send_msg(title=title, text=text, image=image, userid=userid) return self.wechat.send_msg(title=message.title, text=message.text,
image=message.image, userid=message.userid)
def post_medias_message(self, title: str, items: List[MediaInfo], @checkMessage(MessageChannel.Wechat)
userid: Union[str, int] = None) -> Optional[bool]: def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]:
""" """
发送媒体信息选择列表 发送媒体信息选择列表
:param title: 标题 :param message: 消息内容
:param items: 消息列表 :param medias: 媒体列表
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
# 先发送标题 # 先发送标题
self.wechat.send_msg(title=title) self.wechat.send_msg(title=message.title)
# 再发送内容 # 再发送内容
return self.wechat.send_medias_msg(medias=items, userid=userid) return self.wechat.send_medias_msg(medias=medias, userid=message.userid)
def post_torrents_message(self, title: str, items: List[Context], @checkMessage(MessageChannel.Wechat)
mediainfo: MediaInfo, def post_torrents_message(self, message: Notification, torrents: List[Context]) -> Optional[bool]:
userid: Union[str, int] = None) -> Optional[bool]:
""" """
发送种子信息选择列表 发送种子信息选择列表
:param title: 标题 :param message: 消息内容
:param items: 消息列表 :param torrents: 种子列表
:param mediainfo: 媒体信息
:param userid: 用户ID
:return: 成功或失败 :return: 成功或失败
""" """
return self.wechat.send_torrents_msg(title=title, torrents=items, mediainfo=mediainfo, userid=userid) return self.wechat.send_torrents_msg(title=message.title, torrents=torrents, userid=message.userid)

View File

@ -191,7 +191,7 @@ class WeChat(metaclass=Singleton):
} }
return self.__post_request(message_url, req_json) return self.__post_request(message_url, req_json)
def send_torrents_msg(self, torrents: List[Context], mediainfo: MediaInfo, def send_torrents_msg(self, torrents: List[Context],
userid: str = "", title: str = "") -> Optional[bool]: userid: str = "", title: str = "") -> Optional[bool]:
""" """
发送列表消息 发送列表消息
@ -213,6 +213,7 @@ class WeChat(metaclass=Singleton):
for context in torrents: for context in torrents:
torrent = context.torrent_info torrent = context.torrent_info
meta = MetaInfo(title=torrent.title, subtitle=torrent.description) meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
mediainfo = context.media_info
torrent_title = f"{index}.【{torrent.site_name}" \ torrent_title = f"{index}.【{torrent.site_name}" \
f"{meta.season_episode} " \ f"{meta.season_episode} " \
f"{meta.resource_term} " \ f"{meta.resource_term} " \

View File

@ -1,5 +1,5 @@
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, List from typing import Optional, Dict, List, Union
from pydantic import BaseModel from pydantic import BaseModel
@ -321,14 +321,12 @@ class Notification(BaseModel):
title: Optional[str] = None title: Optional[str] = None
# 文本内容 # 文本内容
text: Optional[str] = None text: Optional[str] = None
# 列表内容
items: Optional[list] = []
# 图片 # 图片
image: Optional[str] = None image: Optional[str] = None
# 链接 # 链接
link: Optional[str] = None link: Optional[str] = None
# 用户ID # 用户ID
user_id: Optional[str] = None userid: Optional[Union[str, int]] = None
class CommingMessage(BaseModel): class CommingMessage(BaseModel):
@ -336,7 +334,7 @@ class CommingMessage(BaseModel):
外来消息 外来消息
""" """
# 用户ID # 用户ID
userid: Optional[str] = None userid: Optional[Union[str, int]] = None
# 用户名称 # 用户名称
username: Optional[str] = None username: Optional[str] = None
# 消息渠道 # 消息渠道

View File

@ -1,6 +1,7 @@
import datetime import datetime
import random import random
from typing import List from typing import List
from datetime import datetime
class TimerUtils: class TimerUtils:
@ -38,8 +39,6 @@ class TimerUtils:
return trigger return trigger
from datetime import datetime, timedelta
@staticmethod @staticmethod
def time_difference(input_datetime: datetime) -> str: def time_difference(input_datetime: datetime) -> str:
""" """