From 0903730ab6f608f85493e8fe2e76f4bb109d4830 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 17 Aug 2023 16:24:24 +0800 Subject: [PATCH] =?UTF-8?q?add=20RSS=E8=AE=A2=E9=98=85=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chain/subscribe.py | 50 ++- app/db/subscribe_oper.py | 6 + app/plugins/autosignin/__init__.py | 40 +- app/plugins/doubanrank/__init__.py | 81 +++- app/plugins/doubansync/__init__.py | 89 ++++- app/plugins/rsssubscribe/__init__.py | 536 +++++++++++++++++++++++++++ 6 files changed, 771 insertions(+), 31 deletions(-) create mode 100644 app/plugins/rsssubscribe/__init__.py diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index 3a1bc893..3896adfa 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -10,6 +10,7 @@ from app.chain.download import DownloadChain from app.chain.search import SearchChain from app.core.config import settings from app.core.context import TorrentInfo, Context, MediaInfo +from app.core.meta import MetaBase from app.core.metainfo import MetaInfo from app.db.models.subscribe import Subscribe from app.db.subscribe_oper import SubscribeOper @@ -33,7 +34,7 @@ class SubscribeChain(ChainBase): super().__init__(db) self.downloadchain = DownloadChain(self._db) self.searchchain = SearchChain(self._db) - self.subscribehelper = SubscribeOper(self._db) + self.subscribeoper = SubscribeOper(self._db) self.siteshelper = SitesHelper() self.message = MessageHelper() self.systemconfig = SystemConfigOper(self._db) @@ -98,8 +99,8 @@ class SubscribeChain(ChainBase): 'lack_episode': kwargs.get('total_episode') }) # 添加订阅 - sid, err_msg = self.subscribehelper.add(mediainfo, doubanid=doubanid, - season=season, username=username, **kwargs) + sid, err_msg = self.subscribeoper.add(mediainfo, doubanid=doubanid, + season=season, username=username, **kwargs) if not sid: logger.error(f'{mediainfo.title_year} {err_msg}') if not exist_ok and message: @@ -126,6 +127,15 @@ class SubscribeChain(ChainBase): # 返回结果 return sid, "" + def exists(self, mediainfo: MediaInfo, meta: MetaBase = None): + """ + 判断订阅是否已存在 + """ + if self.subscribeoper.exists(tmdbid=mediainfo.tmdb_id, + season=meta.begin_season if meta else None): + return True + return False + def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None): """ 远程刷新订阅,发送消息 @@ -147,7 +157,7 @@ class SubscribeChain(ChainBase): return if arg_str: sid = int(arg_str) - subscribe = self.subscribehelper.get(sid) + subscribe = self.subscribeoper.get(sid) if not subscribe: self.post_message(Notification(channel=channel, title=f"订阅编号 {sid} 不存在!", userid=userid)) @@ -174,15 +184,15 @@ class SubscribeChain(ChainBase): :return: 更新订阅状态为R或删除订阅 """ if sid: - subscribes = [self.subscribehelper.get(sid)] + subscribes = [self.subscribeoper.get(sid)] else: - subscribes = self.subscribehelper.list(state) + subscribes = self.subscribeoper.list(state) # 遍历订阅 for subscribe in subscribes: logger.info(f'开始搜索订阅,标题:{subscribe.name} ...') # 如果状态为N则更新为R if subscribe.state == 'N': - self.subscribehelper.update(subscribe.id, {'state': 'R'}) + self.subscribeoper.update(subscribe.id, {'state': 'R'}) # 生成元数据 meta = MetaInfo(subscribe.name) meta.year = subscribe.year @@ -200,7 +210,7 @@ class SubscribeChain(ChainBase): exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo) if exist_flag: logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅') - self.subscribehelper.delete(subscribe.id) + self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year}{meta.season} 已完成订阅', @@ -328,7 +338,7 @@ class SubscribeChain(ChainBase): if not subscribe.best_version: # 全部下载完成 logger.info(f'{mediainfo.title_year} 下载完成,完成订阅') - self.subscribehelper.delete(subscribe.id) + self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year}{meta.season} 已完成订阅', @@ -338,7 +348,7 @@ class SubscribeChain(ChainBase): priority = max([item.torrent_info.pri_order for item in downloads]) if priority == 100: logger.info(f'{mediainfo.title_year} 洗版完成,删除订阅') - self.subscribehelper.delete(subscribe.id) + self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year}{meta.season} 已洗版完成', @@ -346,7 +356,7 @@ class SubscribeChain(ChainBase): else: # 正在洗版,更新资源优先级 logger.info(f'{mediainfo.title_year} 正在洗版,更新资源优先级') - self.subscribehelper.update(subscribe.id, { + self.subscribeoper.update(subscribe.id, { "current_priority": priority }) @@ -355,7 +365,7 @@ class SubscribeChain(ChainBase): 刷新站点最新资源 """ # 所有订阅 - subscribes = self.subscribehelper.list('R') + subscribes = self.subscribeoper.list('R') if not subscribes: # 没有订阅不运行 return @@ -428,7 +438,7 @@ class SubscribeChain(ChainBase): logger.warn('没有缓存资源,无法匹配订阅') return # 所有订阅 - subscribes = self.subscribehelper.list('R') + subscribes = self.subscribeoper.list('R') # 遍历订阅 for subscribe in subscribes: logger.info(f'开始匹配订阅,标题:{subscribe.name} ...') @@ -448,7 +458,7 @@ class SubscribeChain(ChainBase): exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo) if exist_flag: logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅') - self.subscribehelper.delete(subscribe.id) + self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year}{meta.season} 已完成订阅', @@ -610,7 +620,7 @@ class SubscribeChain(ChainBase): # 合并已下载集 note = list(set(note).union(set(episodes))) # 更新订阅 - self.subscribehelper.update(subscribe.id, { + self.subscribeoper.update(subscribe.id, { "note": json.dumps(note) }) @@ -643,12 +653,12 @@ class SubscribeChain(ChainBase): logger.info(f'{mediainfo.title_year} 季 {season} 更新缺失集数为{len(left_episodes)} ...') if update_date: # 同时更新最后时间 - self.subscribehelper.update(subscribe.id, { + self.subscribeoper.update(subscribe.id, { "lack_episode": len(left_episodes), "last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) else: - self.subscribehelper.update(subscribe.id, { + self.subscribeoper.update(subscribe.id, { "lack_episode": len(left_episodes) }) @@ -656,7 +666,7 @@ class SubscribeChain(ChainBase): """ 查询订阅并发送消息 """ - subscribes = self.subscribehelper.list() + subscribes = self.subscribeoper.list() if not subscribes: self.post_message(Notification(channel=channel, title='没有任何订阅!', userid=userid)) @@ -695,13 +705,13 @@ class SubscribeChain(ChainBase): if not arg_str.isdigit(): continue subscribe_id = int(arg_str) - subscribe = self.subscribehelper.get(subscribe_id) + subscribe = self.subscribeoper.get(subscribe_id) if not subscribe: self.post_message(Notification(channel=channel, title=f"订阅编号 {subscribe_id} 不存在!", userid=userid)) return # 删除订阅 - self.subscribehelper.delete(subscribe_id) + self.subscribeoper.delete(subscribe_id) # 重新发送消息 self.remote_list(channel, userid) diff --git a/app/db/subscribe_oper.py b/app/db/subscribe_oper.py index a11610cb..bbd2c61d 100644 --- a/app/db/subscribe_oper.py +++ b/app/db/subscribe_oper.py @@ -32,6 +32,12 @@ class SubscribeOper(DbOper): else: return subscribe.id, "订阅已存在" + def exists(self, tmdbid: int, season: int): + """ + 判断是否存在 + """ + return True if Subscribe.exists(self._db, tmdbid=tmdbid, season=season) else False + def get(self, sid: int) -> Subscribe: """ 获取订阅 diff --git a/app/plugins/autosignin/__init__.py b/app/plugins/autosignin/__init__.py index b210b01e..e72b8e29 100644 --- a/app/plugins/autosignin/__init__.py +++ b/app/plugins/autosignin/__init__.py @@ -59,6 +59,7 @@ class AutoSignIn(_PluginBase): # 配置属性 _enabled: bool = False _cron: str = "" + _onlyonce: bool = False _notify: bool = False _queue_cnt: int = 5 _sign_sites: list = [] @@ -74,12 +75,13 @@ class AutoSignIn(_PluginBase): if config: self._enabled = config.get("enabled") self._cron = config.get("cron") + self._onlyonce = config.get("onlyonce") self._notify = config.get("notify") self._queue_cnt = config.get("queue_cnt") or 5 self._sign_sites = config.get("sign_sites") # 加载模块 - if self._enabled: + if self._enabled or self._onlyonce: self._site_schema = ModuleHelper.load('app.plugins.autosignin.sites', filter_func=lambda _, obj: hasattr(obj, 'match')) @@ -108,6 +110,21 @@ class AutoSignIn(_PluginBase): hour=trigger.hour, minute=trigger.minute, name="站点自动签到") + if self._onlyonce: + # 关闭一次性开关 + self._onlyonce = False + # 保存配置 + self.update_config( + { + "enabled": self._enabled, + "notify": self._notify, + "cron": self._cron, + "onlyonce": self._onlyonce, + "queue_cnt": self._queue_cnt, + "sign_sites": self._sign_sites + } + ) + # 启动任务 if self._scheduler.get_jobs(): self._scheduler.print_jobs() @@ -165,7 +182,7 @@ class AutoSignIn(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -181,7 +198,7 @@ class AutoSignIn(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -192,6 +209,22 @@ class AutoSignIn(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] } ] }, @@ -259,6 +292,7 @@ class AutoSignIn(_PluginBase): "enabled": False, "notify": True, "cron": "", + "onlyonce": False, "queue_cnt": 5, "sign_sites": [] } diff --git a/app/plugins/doubanrank/__init__.py b/app/plugins/doubanrank/__init__.py index 35709df9..d9d8a1ac 100644 --- a/app/plugins/doubanrank/__init__.py +++ b/app/plugins/doubanrank/__init__.py @@ -57,9 +57,12 @@ class DoubanRank(_PluginBase): } _enabled = False _cron = "" + _onlyonce = False _rss_addrs = [] _ranks = [] _vote = 0 + _clear = False + _clearflag = False def init_plugin(self, config: dict = None): self.downloadchain = DownloadChain() @@ -68,6 +71,7 @@ class DoubanRank(_PluginBase): if config: self._enabled = config.get("enabled") self._cron = config.get("cron") + self._onlyonce = config.get("onlyonce") self._vote = float(config.get("vote")) if config.get("vote") else 0 rss_addrs = config.get("rss_addrs") if rss_addrs: @@ -78,12 +82,13 @@ class DoubanRank(_PluginBase): else: self._rss_addrs = [] self._ranks = config.get("ranks") or [] + self._clear = config.get("clear") # 停止现有任务 self.stop_service() # 启动服务 - if self._enabled: + if self._enabled or self._onlyonce: self._scheduler = BackgroundScheduler(timezone=settings.TZ) if self._cron: logger.info(f"豆瓣榜单订阅服务启动,周期:{self._cron}") @@ -100,6 +105,16 @@ class DoubanRank(_PluginBase): name="豆瓣榜单订阅") logger.info("豆瓣榜单订阅服务启动,周期:每天 08:00") + if self._onlyonce or self._clear: + # 关闭一次性开关 + self._onlyonce = False + # 记录缓存清理标志 + self._clearflag = self._clear + # 关闭清理缓存 + self._clear = False + # 保存配置 + self.__update_config() + if self._scheduler.get_jobs(): # 启动服务 self._scheduler.print_jobs() @@ -126,7 +141,8 @@ class DoubanRank(_PluginBase): { 'component': 'VCol', 'props': { - 'cols': 12 + 'cols': 12, + 'md': 6 }, 'content': [ { @@ -137,6 +153,22 @@ class DoubanRank(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] } ] }, @@ -224,15 +256,38 @@ class DoubanRank(_PluginBase): ] } ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'clear', + 'label': '清理历史记录', + } + } + ] + } + ] } ] } ], { "enabled": False, "cron": "", + "onlyonce": False, "vote": "", "ranks": [], "rss_addrs": "", + "clear": False } def get_page(self) -> List[dict]: @@ -353,6 +408,20 @@ class DoubanRank(_PluginBase): except Exception as e: print(str(e)) + def __update_config(self): + """ + 列新配置 + """ + self.update_config({ + "enabled": self._enabled, + "cron": self._cron, + "onlyonce": self._onlyonce, + "vote": self._vote, + "ranks": self._ranks, + "rss_addrs": self._rss_addrs, + "clear": self._clear + }) + def __refresh_rss(self): """ 刷新RSS @@ -366,7 +435,10 @@ class DoubanRank(_PluginBase): logger.info(f"共 {len(addr_list)} 个榜单RSS地址需要刷新") # 读取历史记录 - history: List[dict] = self.get_data('history') or [] + if self._clearflag: + history = [] + else: + history: List[dict] = self.get_data('history') or [] for addr in addr_list: if not addr: @@ -440,7 +512,8 @@ class DoubanRank(_PluginBase): # 保存历史记录 self.save_data('history', history) - + # 缓存只清理一次 + self._clearflag = False logger.info(f"所有榜单RSS刷新完成") @staticmethod diff --git a/app/plugins/doubansync/__init__.py b/app/plugins/doubansync/__init__.py index 48444c42..4d5667ec 100644 --- a/app/plugins/doubansync/__init__.py +++ b/app/plugins/doubansync/__init__.py @@ -3,6 +3,7 @@ from pathlib import Path from threading import Lock from typing import Optional, Any, List, Dict, Tuple +import pytz from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger @@ -55,10 +56,13 @@ class DoubanSync(_PluginBase): # 配置属性 _enabled: bool = False + _onlyonce: bool = False _cron: str = "" _notify: bool = False _days: int = 7 _users: str = "" + _clear: bool = False + _clearflag: bool = False def init_plugin(self, config: dict = None): self.rsshelper = RssHelper() @@ -76,8 +80,10 @@ class DoubanSync(_PluginBase): self._notify = config.get("notify") self._days = config.get("days") self._users = config.get("users") + self._onlyonce = config.get("onlyonce") + self._clear = config.get("clear") - if self._enabled: + if self._enabled or self._onlyonce: self._scheduler = BackgroundScheduler(timezone=settings.TZ) if self._cron: @@ -92,6 +98,23 @@ class DoubanSync(_PluginBase): else: self._scheduler.add_job(self.sync, "interval", minutes=30, name="豆瓣想看") + if self._onlyonce: + logger.info(f"豆瓣想看服务启动,立即运行一次") + self._scheduler.add_job(func=self.sync, trigger='date', + run_date=datetime.datetime.now( + tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3) + ) + + if self._onlyonce or self._clear: + # 关闭一次性开关 + self._onlyonce = False + # 记录缓存清理标志 + self._clearflag = self._clear + # 关闭清理缓存 + self._clear = False + # 保存配置 + self.__update_config() + # 启动任务 if self._scheduler.get_jobs(): self._scheduler.print_jobs() @@ -140,7 +163,7 @@ class DoubanSync(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -156,7 +179,7 @@ class DoubanSync(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 6 + 'md': 4 }, 'content': [ { @@ -167,6 +190,22 @@ class DoubanSync(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] } ] }, @@ -225,15 +264,38 @@ class DoubanSync(_PluginBase): ] } ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'clear', + 'label': '清理历史记录', + } + } + ] + } + ] } ] } ], { "enabled": False, "notify": True, + "onlyonce": False, "cron": "*/30 * * * *", "days": 7, "users": "", + "clear": False } def get_page(self) -> List[dict]: @@ -339,6 +401,20 @@ class DoubanSync(_PluginBase): } ] + def __update_config(self): + """ + 更新配置 + """ + self.update_config({ + "enabled": self._enabled, + "notify": self._notify, + "onlyonce": self._onlyonce, + "cron": self._cron, + "days": self._days, + "users": self._users, + "clear": self._clear + }) + def stop_service(self): """ 退出插件 @@ -359,7 +435,10 @@ class DoubanSync(_PluginBase): if not self._users: return # 读取历史记录 - history: List[dict] = self.get_data('history') or [] + if self._clearflag: + history = [] + else: + history: List[dict] = self.get_data('history') or [] for user_id in self._users.split(","): # 同步每个用户的豆瓣数据 if not user_id: @@ -460,6 +539,8 @@ class DoubanSync(_PluginBase): logger.info(f"用户 {user_id} 豆瓣想看同步完成") # 保存历史记录 self.save_data('history', history) + # 缓存只清理一次 + self._clearflag = False @eventmanager.register(EventType.DoubanSync) def remote_sync(self, event: Event): diff --git a/app/plugins/rsssubscribe/__init__.py b/app/plugins/rsssubscribe/__init__.py new file mode 100644 index 00000000..8bccaf35 --- /dev/null +++ b/app/plugins/rsssubscribe/__init__.py @@ -0,0 +1,536 @@ +import datetime +import re +from pathlib import Path +from threading import Lock +from typing import Optional, Any, List, Dict, Tuple + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +from app.chain.download import DownloadChain +from app.chain.search import SearchChain +from app.chain.subscribe import SubscribeChain +from app.core.config import settings +from app.core.context import MediaInfo +from app.core.metainfo import MetaInfo +from app.helper.rss import RssHelper +from app.log import logger +from app.plugins import _PluginBase + +lock = Lock() + + +class RssSubscribe(_PluginBase): + # 插件名称 + plugin_name = "RSS订阅" + # 插件描述 + plugin_desc = "定时刷新RSS报文,识别报文内容并自动添加订阅。" + # 插件图标 + plugin_icon = "rss.png" + # 主题色 + plugin_color = "#F78421" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "rsssubscribe_" + # 加载顺序 + plugin_order = 19 + # 可使用的用户级别 + auth_level = 2 + + # 私有变量 + _scheduler: Optional[BackgroundScheduler] = None + _cache_path: Optional[Path] = None + rsshelper = None + downloadchain = None + searchchain = None + subscribechain = None + + # 配置属性 + _enabled: bool = False + _cron: str = "" + _notify: bool = False + _onlyonce: bool = False + _address: str = "" + _include: str = "" + _exclude: str = "" + _proxy: bool = False + _clear: bool = False + _clearflag: bool = False + + def init_plugin(self, config: dict = None): + self.rsshelper = RssHelper() + self.downloadchain = DownloadChain() + self.searchchain = SearchChain() + self.subscribechain = SubscribeChain() + + # 停止现有任务 + self.stop_service() + + # 配置 + if config: + self._enabled = config.get("enabled") + self._cron = config.get("cron") + self._notify = config.get("notify") + self._onlyonce = config.get("onlyonce") + self._address = config.get("address") + self._include = config.get("include") + self._exclude = config.get("exclude") + self._proxy = config.get("proxy") + self._clear = config.get("clear") + + if self._enabled or self._onlyonce: + + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + if self._cron: + try: + self._scheduler.add_job(func=self.check, + trigger=CronTrigger.from_crontab(self._cron), + name="RSS订阅") + except Exception as err: + logger.error(f"定时任务配置错误:{err}") + # 推送实时消息 + self.systemmessage.put(f"执行周期配置错误:{err}") + else: + self._scheduler.add_job(self.check, "interval", minutes=30, name="RSS订阅") + + if self._onlyonce: + logger.info(f"RSS订阅服务启动,立即运行一次") + self._scheduler.add_job(func=self.check, trigger='date', + run_date=datetime.datetime.now( + tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3) + ) + + if self._onlyonce or self._clear: + # 关闭一次性开关 + self._onlyonce = False + # 记录清理缓存设置 + self._clearflag = self._clear + # 关闭清理缓存开关 + self._clearflag = False + # 保存设置 + self.__update_config() + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + def get_state(self) -> bool: + return self._enabled + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + """ + 定义远程控制命令 + :return: 命令关键字、事件、描述、附带数据 + """ + pass + + def get_api(self) -> List[Dict[str, Any]]: + """ + 获取插件API + [{ + "path": "/xx", + "endpoint": self.xxx, + "methods": ["GET", "POST"], + "summary": "API说明" + }] + """ + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'notify', + 'label': '发送通知', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12 + }, + 'content': [ + { + 'component': 'VTextarea', + 'props': { + 'model': 'address', + 'label': 'RSS地址', + 'rows': 5, + 'placeholder': '每行一个RSS地址' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'include', + 'label': '包含', + 'placeholder': '支持正则表达式' + } + } + ] + }, + { + 'component': 'VCol', + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'exclude', + 'label': '排除', + 'placeholder': '支持正则表达式' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'proxy', + 'label': '使用代理服务器', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'clear', + 'label': '清理历史记录', + } + } + ] + } + ] + } + ] + } + ], { + "enabled": False, + "notify": True, + "onlyonce": False, + "cron": "*/30 * * * *", + "address": "", + "include": "", + "exclude": "", + "proxy": False, + "clear": False + } + + def get_page(self) -> List[dict]: + """ + 拼装插件详情页面,需要返回页面配置,同时附带数据 + """ + # 查询同步详情 + historys = self.get_data('history') + if not historys: + return [ + { + 'component': 'div', + 'text': '暂无数据', + 'props': { + 'class': 'text-center', + } + } + ] + # 数据按时间降序排序 + historys = sorted(historys, key=lambda x: x.get('time'), reverse=True) + # 拼装页面 + contents = [] + for history in historys: + title = history.get("title") + poster = history.get("poster") + mtype = history.get("type") + time_str = history.get("time") + contents.append( + { + 'component': 'VCard', + 'content': [ + { + 'component': 'div', + 'props': { + 'class': 'd-flex justify-space-start flex-nowrap flex-row', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': poster, + 'height': 120, + 'width': 80, + 'aspect-ratio': '2/3', + 'class': 'object-cover shadow ring-gray-500', + 'cover': True + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'VCardSubtitle', + 'props': { + 'class': 'pa-2 font-bold break-words whitespace-break-spaces' + }, + 'text': title + }, + { + 'component': 'VCardText', + 'props': { + 'class': 'pa-0 px-2' + }, + 'text': f'类型:{mtype}' + }, + { + 'component': 'VCardText', + 'props': { + 'class': 'pa-0 px-2' + }, + 'text': f'时间:{time_str}' + } + ] + } + ] + } + ] + } + ) + + return [ + { + 'component': 'div', + 'props': { + 'class': 'grid gap-3 grid-info-card', + }, + 'content': contents + } + ] + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error("退出插件失败:%s" % str(e)) + + def __update_config(self): + """ + 更新设置 + """ + self.update_config({ + "enabled": self._enabled, + "notify": self._notify, + "onlyonce": self._onlyonce, + "cron": self._cron, + "address": self._address, + "include": self._include, + "exclude": self._exclude, + "proxy": self._proxy, + "clear": self._clear + }) + + def check(self): + """ + 通过用户RSS同步豆瓣想看数据 + """ + if not self._address: + return + # 读取历史记录 + if self._clearflag: + history = [] + else: + history: List[dict] = self.get_data('history') or [] + for url in self._address.split("\n"): + # 处理每一个RSS链接 + if not url: + continue + logger.info(f"开始刷新RSS:{url} ...") + results = self.rsshelper.parse(url, proxy=self._proxy) + if not results: + logger.error(f"未获取到RSS数据:{url}") + return + # 解析数据 + for result in results: + try: + title = result.get("title") + description = result.get("description") + # 检查是否处理过 + if not title or title in [h.get("title") for h in history]: + continue + # 检查规则 + if self._include and not re.search(r"%s" % self._include, + f"{title} {description}", re.IGNORECASE): + logger.info(f"{title} - {description} 不符合包含规则") + continue + if self._exclude and re.search(r"%s" % self._exclude, + f"{title} {description}", re.IGNORECASE): + logger.info(f"{title} - {description} 不符合排除规则") + continue + # 识别媒体信息 + meta = MetaInfo(title=title, subtitle=description) + if not meta.name: + logger.warn(f"{title} 未识别到有效数据") + continue + mediainfo: MediaInfo = self.chain.recognize_media(meta=meta) + if not mediainfo: + logger.warn(f'未识别到媒体信息,标题:{title}') + continue + # 查询缺失的媒体信息 + exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo) + if exist_flag: + logger.info(f'{mediainfo.title_year} 媒体库中已存在') + continue + else: + # 检查是否在订阅中 + subflag = self.subscribechain.exists(mediainfo=mediainfo, meta=meta) + if subflag: + logger.info(f'{mediainfo.title_year}{meta.season} 正在订阅中') + continue + # 添加订阅 + self.subscribechain.add(title=mediainfo.title, + year=mediainfo.year, + mtype=mediainfo.type, + tmdbid=mediainfo.tmdb_id, + season=meta.begin_season, + exist_ok=True, + username="RSS订阅") + # 存储历史记录 + history.append({ + "title": f"{mediainfo.title} {meta.season}", + "type": mediainfo.type.value, + "year": mediainfo.year, + "poster": mediainfo.get_poster_image(), + "overview": mediainfo.overview, + "tmdbid": mediainfo.tmdb_id, + "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }) + except Exception as err: + logger.error(f'刷新RSS数据出错:{err}') + logger.info(f"RSS {url} 刷新完成") + # 保存历史记录 + self.save_data('history', history) + # 缓存只清理一次 + self._clearflag = False