diff --git a/README.md b/README.md index 8eb31f99..2789fa9b 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,14 @@ location / { proxy_set_header X-Forwarded-Proto $scheme; } ``` +- 反代使用ssl时,需要开启`http2`,否则会导致日志加载时间过长或不可用。以`Nginx`为例: +```nginx configuration +server{ + listen 443 ssl; + http2 on; + ... +} +``` - 新建的企业微信应用需要固定公网IP的代理才能收到消息,代理添加以下代码: ```nginx configuration location /cgi-bin/gettoken { diff --git a/app/api/endpoints/douban.py b/app/api/endpoints/douban.py index 38a6c918..d4981f58 100644 --- a/app/api/endpoints/douban.py +++ b/app/api/endpoints/douban.py @@ -90,7 +90,7 @@ def movie_top250(page: int = 1, """ 浏览豆瓣剧集信息 """ - movies = DoubanChain().movie_top250(page=page, count=count) + movies = DoubanChain().movie_top250(page=page, count=count) or [] return [MediaInfo(douban_info=movie).to_dict() for movie in movies] @@ -101,7 +101,7 @@ def tv_weekly_chinese(page: int = 1, """ 中国每周剧集口碑榜 """ - tvs = DoubanChain().tv_weekly_chinese(page=page, count=count) + tvs = DoubanChain().tv_weekly_chinese(page=page, count=count) or [] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] @@ -112,7 +112,7 @@ def tv_weekly_global(page: int = 1, """ 全球每周剧集口碑榜 """ - tvs = DoubanChain().tv_weekly_global(page=page, count=count) + tvs = DoubanChain().tv_weekly_global(page=page, count=count) or [] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] @@ -123,7 +123,7 @@ def tv_animation(page: int = 1, """ 热门动画剧集 """ - tvs = DoubanChain().tv_animation(page=page, count=count) + tvs = DoubanChain().tv_animation(page=page, count=count) or [] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] @@ -134,7 +134,7 @@ def movie_hot(page: int = 1, """ 热门电影 """ - movies = DoubanChain().movie_hot(page=page, count=count) + movies = DoubanChain().movie_hot(page=page, count=count) or [] return [MediaInfo(douban_info=movie).to_dict() for movie in movies] @@ -145,7 +145,7 @@ def tv_hot(page: int = 1, """ 热门电视剧 """ - tvs = DoubanChain().tv_hot(page=page, count=count) + tvs = DoubanChain().tv_hot(page=page, count=count) or [] return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index 7d9d5443..7d107d71 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -113,20 +113,6 @@ def media_info(mediaid: str, type_name: str, doubanid = mediaid[7:] if not tmdbid and not doubanid: return schemas.MediaInfo() - if settings.RECOGNIZE_SOURCE == "themoviedb": - if not tmdbid and doubanid: - tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype) - if tmdbinfo: - tmdbid = tmdbinfo.get("id") - else: - return schemas.MediaInfo() - else: - if not doubanid and tmdbid: - doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype) - if doubaninfo: - doubanid = doubaninfo.get("id") - else: - return schemas.MediaInfo() mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype) if mediainfo: MediaChain().obtain_images(mediainfo) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 90de59d9..fa0f1300 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -8,11 +8,13 @@ from app import schemas from app.chain.message import MessageChain from app.core.config import settings from app.core.security import verify_token +from app.db.models import User from app.db.systemconfig_oper import SystemConfigOper +from app.db.userauth import get_current_active_superuser from app.log import logger from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt from app.schemas import NotificationSwitch -from app.schemas.types import SystemConfigKey, NotificationType +from app.schemas.types import SystemConfigKey, NotificationType, MessageChannel router = APIRouter() @@ -36,6 +38,20 @@ async def user_message(background_tasks: BackgroundTasks, request: Request): return schemas.Response(success=True) +@router.post("/web", summary="接收WEB消息", response_model=schemas.Response) +async def web_message(text: str, current_user: User = Depends(get_current_active_superuser)): + """ + WEB消息响应 + """ + MessageChain().handle_message( + channel=MessageChannel.Web, + userid=current_user.id, + username=current_user.name, + text=text + ) + return schemas.Response(success=True) + + def wechat_verify(echostr: str, msg_signature: str, timestamp: Union[str, int], nonce: str) -> Any: """ @@ -103,7 +119,7 @@ def read_switchs(_: schemas.TokenPayload = Depends(verify_token)) -> Any: def set_switchs(switchs: List[NotificationSwitch], _: schemas.TokenPayload = Depends(verify_token)) -> Any: """ - 查询通知消息渠道开关 + 设置通知消息渠道开关 """ switch_list = [] for switch in switchs: diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 891b3713..09b031cf 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -138,7 +138,7 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None, @router.get("/message", summary="实时消息") -def get_message(token: str): +def get_message(token: str, role: str = "sys"): """ 实时获取系统消息,返回格式为SSE """ @@ -152,7 +152,7 @@ def get_message(token: str): def event_generator(): while True: - detail = message.get() + detail = message.get(role) yield 'data: %s\n\n' % (detail or '') time.sleep(3) diff --git a/app/chain/__init__.py b/app/chain/__init__.py index e0bcd08e..fc9ee27a 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -15,6 +15,8 @@ from app.core.context import MediaInfo, TorrentInfo from app.core.event import EventManager from app.core.meta import MetaBase from app.core.module import ModuleManager +from app.db.message_oper import MessageOper +from app.helper.message import MessageHelper from app.log import logger from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \ WebhookEventInfo, TmdbEpisode @@ -33,6 +35,8 @@ class ChainBase(metaclass=ABCMeta): """ self.modulemanager = ModuleManager() self.eventmanager = EventManager() + self.messageoper = MessageOper() + self.messagehelper = MessageHelper() @staticmethod def load_cache(filename: str) -> Any: @@ -403,6 +407,10 @@ class ChainBase(metaclass=ABCMeta): :param message: 消息体 :return: 成功或失败 """ + logger.info(f"发送消息:channel={message.channel}," + f"title={message.title}, " + f"text={message.text}," + f"userid={message.userid}") # 发送事件 self.eventmanager.send_event(etype=EventType.NoticeMessage, data={ @@ -413,10 +421,13 @@ class ChainBase(metaclass=ABCMeta): "image": message.image, "userid": message.userid, }) - logger.info(f"发送消息:channel={message.channel}," - f"title={message.title}, " - f"text={message.text}," - f"userid={message.userid}") + # 保存消息 + self.messagehelper.put(message, role="user") + self.messageoper.add(channel=message.channel, mtype=message.mtype, + title=message.title, text=message.text, + image=message.image, link=message.link, + userid=message.userid, action=1) + # 发送 self.run_module("post_message", message=message) def post_medias_message(self, message: Notification, medias: List[MediaInfo]) -> Optional[bool]: diff --git a/app/chain/message.py b/app/chain/message.py index 28a346d6..110dae79 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -12,9 +12,11 @@ from app.core.config import settings from app.core.context import MediaInfo, Context from app.core.event import EventManager from app.core.meta import MetaBase +from app.db.message_oper import MessageOper +from app.helper.message import MessageHelper from app.helper.torrent import TorrentHelper from app.log import logger -from app.schemas import Notification, NotExistMediaInfo +from app.schemas import Notification, NotExistMediaInfo, CommingMessage from app.schemas.types import EventType, MessageChannel, MediaType from app.utils.string import StringUtils @@ -43,6 +45,8 @@ class MessageChain(ChainBase): self.mediachain = MediaChain() self.eventmanager = EventManager() self.torrenthelper = TorrentHelper() + self.messagehelper = MessageHelper() + self.messageoper = MessageOper() def __get_noexits_info( self, @@ -100,10 +104,8 @@ class MessageChain(ChainBase): def process(self, body: Any, form: Any, args: Any) -> None: """ - 识别消息内容,执行操作 + 调用模块识别消息内容 """ - # 申明全局变量 - global _current_page, _current_meta, _current_media # 获取消息内容 info = self.message_parser(body=body, form=form, args=args) if not info: @@ -122,10 +124,35 @@ class MessageChain(ChainBase): if not text: logger.debug(f'未识别到消息内容::{body}{form}{args}') return + # 处理消息 + self.handle_message(channel=channel, userid=userid, username=username, text=text) + + def handle_message(self, channel: MessageChannel, userid: Union[str, int], username: str, text: str) -> None: + """ + 识别消息内容,执行操作 + """ + # 申明全局变量 + global _current_page, _current_meta, _current_media # 加载缓存 user_cache: Dict[str, dict] = self.load_cache(self._cache_file) or {} # 处理消息 logger.info(f'收到用户消息内容,用户:{userid},内容:{text}') + # 保存消息 + self.messagehelper.put( + CommingMessage( + userid=userid, + username=username, + channel=channel, + text=text + ), role="user") + self.messageoper.add( + channel=channel, + userid=userid, + username=username, + text=text, + action=0 + ) + # 处理消息 if text.startswith('/'): # 执行命令 self.eventmanager.send_event( diff --git a/app/chain/search.py b/app/chain/search.py index eb34d9a9..12c2423c 100644 --- a/app/chain/search.py +++ b/app/chain/search.py @@ -124,14 +124,12 @@ class SearchChain(ChainBase): if keyword: keywords = [keyword] else: - keywords = list( - { - mediainfo.title, - mediainfo.original_title, - mediainfo.en_title, - mediainfo.sg_title - } - {None} - ) + # 去重去空,但要保持顺序 + keywords = list(dict.fromkeys([k for k in [mediainfo.title, + mediainfo.original_title, + mediainfo.en_title, + mediainfo.sg_title] if k])) + # 执行搜索 torrents: List[TorrentInfo] = self.__search_all_sites( mediainfo=mediainfo, diff --git a/app/chain/site.py b/app/chain/site.py index 6fc652cf..c4446525 100644 --- a/app/chain/site.py +++ b/app/chain/site.py @@ -302,20 +302,21 @@ class SiteChain(ChainBase): if not site_info: return False, f"站点【{url}】不存在" - # 特殊站点测试 - if self.special_site_test.get(domain): - return self.special_site_test[domain](site_info) - - # 通用站点测试 - site_url = site_info.url - site_cookie = site_info.cookie - ua = site_info.ua - render = site_info.render - public = site_info.public - proxies = settings.PROXY if site_info.proxy else None - proxy_server = settings.PROXY_SERVER if site_info.proxy else None # 模拟登录 try: + # 特殊站点测试 + if self.special_site_test.get(domain): + return self.special_site_test[domain](site_info) + + # 通用站点测试 + site_url = site_info.url + site_cookie = site_info.cookie + ua = site_info.ua + render = site_info.render + public = site_info.public + proxies = settings.PROXY if site_info.proxy else None + proxy_server = settings.PROXY_SERVER if site_info.proxy else None + # 访问链接 if render: page_source = PlaywrightHelper().get_page_source(url=site_url, diff --git a/app/db/message_oper.py b/app/db/message_oper.py new file mode 100644 index 00000000..b296b7de --- /dev/null +++ b/app/db/message_oper.py @@ -0,0 +1,57 @@ +import time +from typing import Optional, Union + +from sqlalchemy.orm import Session + +from app.db import DbOper +from app.db.models.message import Message +from app.schemas import MessageChannel, NotificationType + + +class MessageOper(DbOper): + """ + 消息数据管理 + """ + + def __init__(self, db: Session = None): + super().__init__(db) + + def add(self, + channel: MessageChannel = None, + mtype: NotificationType = None, + title: str = None, + text: str = None, + image: str = None, + link: str = None, + userid: str = None, + action: int = 1, + **kwargs): + """ + 新增媒体服务器数据 + :param channel: 消息渠道 + :param mtype: 消息类型 + :param title: 标题 + :param text: 文本内容 + :param image: 图片 + :param link: 链接 + :param userid: 用户ID + :param action: 消息方向:0-接收息,1-发送消息 + """ + kwargs.update({ + "channel": channel.value if channel else '', + "mtype": mtype.value if mtype else '', + "title": title, + "text": text, + "image": image, + "link": link, + "userid": userid, + "action": action, + "reg_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + }) + Message(**kwargs).create(self._db) + + def list_by_page(self, page: int = 1, count: int = 30) -> Optional[str]: + """ + 获取媒体服务器数据ID + """ + return Message.list_by_page(self._db, page, count) diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index 1fd5c167..27d5c94a 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -200,6 +200,7 @@ class DownloadFiles(Base): result = db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all() return list(result) + @staticmethod @db_update def delete_by_fullpath(db: Session, fullpath: str): db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath, diff --git a/app/db/models/message.py b/app/db/models/message.py new file mode 100644 index 00000000..a14760d8 --- /dev/null +++ b/app/db/models/message.py @@ -0,0 +1,36 @@ +from sqlalchemy import Column, Integer, String, Sequence +from sqlalchemy.orm import Session + +from app.db import db_query, Base + + +class Message(Base): + """ + 消息表 + """ + id = Column(Integer, Sequence('id'), primary_key=True, index=True) + # 消息渠道 + channel = Column(String, nullable=False) + # 消息类型 + mtype = Column(String, nullable=False) + # 标题 + title = Column(String) + # 文本内容 + text = Column(String) + # 图片 + image = Column(String) + # 链接 + link = Column(String) + # 用户ID + userid = Column(String) + # 登记时间 + reg_time = Column(String) + # 消息方向:0-接收息,1-发送消息 + action = Column(Integer) + + @staticmethod + @db_query + def list_by_page(db: Session, page: int = 1, count: int = 30): + result = db.query(Message).order_by(Message.reg_time.desc()).offset((page - 1) * count).limit( + count).all() + return list(result) diff --git a/app/helper/message.py b/app/helper/message.py index d344faad..eaa9475c 100644 --- a/app/helper/message.py +++ b/app/helper/message.py @@ -1,19 +1,41 @@ +import json import queue +from typing import Optional, Any from app.utils.singleton import Singleton class MessageHelper(metaclass=Singleton): """ - 消息队列管理器 + 消息队列管理器,包括系统消息和用户消息 """ def __init__(self): - self.queue = queue.Queue() + self.sys_queue = queue.Queue() + self.user_queue = queue.Queue() - def put(self, message: str): - self.queue.put(message) + def put(self, message: Any, role: str = "sys"): + """ + 存消息 + :param message: 消息 + :param role: 消息通道 sys/user + """ + if role == "sys": + self.sys_queue.put(message) + else: + if isinstance(message, str): + self.user_queue.put(message) + elif hasattr(message, "dict"): + self.user_queue.put(json.dumps(message.dict())) - def get(self): - if not self.queue.empty(): - return self.queue.get(block=False) + def get(self, role: str = "sys") -> Optional[str]: + """ + 取消息 + :param role: 消息通道 sys/user + """ + if role == "sys": + if not self.sys_queue.empty(): + return self.sys_queue.get(block=False) + else: + if not self.user_queue.empty(): + return self.user_queue.get(block=False) return None diff --git a/app/modules/douban/__init__.py b/app/modules/douban/__init__.py index ee7639c5..4eea1c7c 100644 --- a/app/modules/douban/__init__.py +++ b/app/modules/douban/__init__.py @@ -57,7 +57,11 @@ class DoubanModule(_ModuleBase): :param cache: 是否使用缓存 :return: 识别的媒体信息,包括剧集信息 """ - if settings.RECOGNIZE_SOURCE != "douban": + if not doubanid and not meta: + return None + + if meta and not doubanid \ + and settings.RECOGNIZE_SOURCE != "douban": return None if not meta: diff --git a/app/modules/filetransfer/__init__.py b/app/modules/filetransfer/__init__.py index 1b697f22..6a28c511 100644 --- a/app/modules/filetransfer/__init__.py +++ b/app/modules/filetransfer/__init__.py @@ -38,12 +38,12 @@ class FileTransferModule(_ModuleBase): for path in [settings.DOWNLOAD_PATH, settings.DOWNLOAD_MOVIE_PATH, settings.DOWNLOAD_TV_PATH, - settings.DWONLOAD_ANIME_PATH]: + settings.DOWNLOAD_ANIME_PATH]: if not path: continue download_path = Path(path) if not download_path.exists(): - return False, f"目录 {download_path} 不存在" + return False, f"下载目录 {download_path} 不存在" download_paths.append(path) # 下载目录的设备ID download_devids = [Path(path).stat().st_dev for path in download_paths] @@ -54,7 +54,7 @@ class FileTransferModule(_ModuleBase): for path in settings.LIBRARY_PATHS: library_path = Path(path) if not library_path.exists(): - return False, f"目录不存在:{library_path}" + return False, f"媒体库目录不存在:{library_path}" if settings.DOWNLOADER_MONITOR and settings.TRANSFER_TYPE == "link": if library_path.stat().st_dev not in download_devids: return False, f"媒体库目录 {library_path} " \ diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index 307c69ae..27dd58c6 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -67,7 +67,11 @@ class TheMovieDbModule(_ModuleBase): :param cache: 是否使用缓存 :return: 识别的媒体信息,包括剧集信息 """ - if settings.RECOGNIZE_SOURCE != "themoviedb": + if not tmdbid and not meta: + return None + + if meta and not tmdbid \ + and settings.RECOGNIZE_SOURCE != "themoviedb": return None if not meta: @@ -182,7 +186,7 @@ class TheMovieDbModule(_ModuleBase): :param season: 季号 """ # 搜索 - logger.info(f"开始使用 名称:{name}、年份:{year} 匹配TMDB信息 ...") + logger.info(f"开始使用 名称:{name} 年份:{year} 匹配TMDB信息 ...") info = self.tmdb.match(name=name, year=year, mtype=mtype, diff --git a/app/modules/themoviedb/tmdbapi.py b/app/modules/themoviedb/tmdbapi.py index 9c9fab3a..ef20dd59 100644 --- a/app/modules/themoviedb/tmdbapi.py +++ b/app/modules/themoviedb/tmdbapi.py @@ -189,9 +189,16 @@ class TmdbHelper: season_year, season_number) if not info: - logger.debug( - f"正在识别{mtype.value}:{name}, 年份={year} ...") - info = self.__search_tv_by_name(name, year) + year_range = [year] + if year: + year_range.append(str(int(year) + 1)) + year_range.append(str(int(year) - 1)) + for year in year_range: + logger.debug( + f"正在识别{mtype.value}:{name}, 年份={year} ...") + info = self.__search_tv_by_name(name, year) + if info: + break if info: info['media_type'] = MediaType.TV # 返回 diff --git a/app/schemas/message.py b/app/schemas/message.py index 051357e1..da125ad1 100644 --- a/app/schemas/message.py +++ b/app/schemas/message.py @@ -18,6 +18,18 @@ class CommingMessage(BaseModel): # 消息体 text: Optional[str] = None + def dict(self): + """ + 转换为字典 + """ + return { + "userid": self.userid, + "username": self.username, + "channel": self.channel.value if self.channel else None, + "text": self.text, + "action": 0 + } + class Notification(BaseModel): """ @@ -38,6 +50,18 @@ class Notification(BaseModel): # 用户ID userid: Optional[Union[str, int]] = None + def dict(self): + return { + "channel": self.channel.value if self.channel else None, + "mtype": self.mtype.value if self.mtype else None, + "title": self.title, + "text": self.text, + "image": self.image, + "link": self.link, + "userid": self.userid, + "action": 1 + } + class NotificationSwitch(BaseModel): """ diff --git a/app/schemas/types.py b/app/schemas/types.py index 528b8300..1f991619 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -117,3 +117,4 @@ class MessageChannel(Enum): Slack = "Slack" SynologyChat = "SynologyChat" VoceChat = "VoceChat" + Web = "Web" diff --git a/version.py b/version.py index 447b04e5..22eb5143 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -APP_VERSION = 'v1.7.1' +APP_VERSION = 'v1.7.2'