import re import xml.dom.minidom from threading import Event from typing import Tuple, List, Dict, Any from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from app.core.config import settings from app.log import logger from app.plugins import _PluginBase from app.utils.dom import DomUtils from app.utils.http import RequestUtils class DoubanRank(_PluginBase): # 插件名称 plugin_name = "豆瓣榜单订阅" # 插件描述 plugin_desc = "监控豆瓣热门榜单,自动添加订阅。" # 插件图标 plugin_icon = "movie.jpg" # 主题色 plugin_color = "#01B3E3" # 插件版本 plugin_version = "1.0" # 插件作者 plugin_author = "jxxghp" # 作者主页 author_url = "https://github.com/jxxghp" # 插件配置项ID前缀 plugin_config_prefix = "doubanrank_" # 加载顺序 plugin_order = 16 # 可使用的用户级别 auth_level = 2 # 退出事件 _event = Event() # 私有属性 mediaserver = None subscribe = None rsshelper = None media = None _douban_address = { 'movie-ustop': 'https://rsshub.app/douban/movie/ustop', 'movie-weekly': 'https://rsshub.app/douban/movie/weekly', 'movie-real-time': 'https://rsshub.app/douban/movie/weekly/subject_real_time_hotest', 'show-domestic': 'https://rsshub.app/douban/movie/weekly/show_domestic', 'movie-hot-gaia': 'https://rsshub.app/douban/movie/weekly/movie_hot_gaia', 'tv-hot': 'https://rsshub.app/douban/movie/weekly/tv_hot', 'movie-top250': 'https://rsshub.app/douban/movie/weekly/movie_top250', } _enable = False _cron = "" _rss_addrs = [] _ranks = [] _vote = 0 _scheduler = None def init_plugin(self, config: dict = None): if config: self._enable = config.get("enable") self._cron = config.get("cron") self._vote = float(config.get("vote")) if config.get("vote") else 0 rss_addrs = config.get("rss_addrs") if rss_addrs: if isinstance(rss_addrs, str): self._rss_addrs = rss_addrs.split('\n') else: self._rss_addrs = rss_addrs else: self._rss_addrs = [] self._ranks = config.get("ranks") or [] # 停止现有任务 self.stop_service() # 启动服务 if self._enable: self._scheduler = BackgroundScheduler(timezone=settings.TZ) if self._cron: logger.info(f"豆瓣榜单订阅服务启动,周期:{self._cron}") self._scheduler.add_job(self.__refresh_rss, CronTrigger.from_crontab(self._cron)) if self._scheduler.get_jobs(): # 启动服务 self._scheduler.print_jobs() self._scheduler.start() @staticmethod def get_command() -> List[Dict[str, Any]]: pass def get_api(self) -> List[Dict[str, Any]]: pass def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: pass def get_page(self) -> List[dict]: """ 拼装插件详情页面,需要返回页面配置,同时附带数据 """ pass def stop_service(self): """ 停止服务 """ try: if self._scheduler: self._scheduler.remove_all_jobs() if self._scheduler.running: self._event.set() self._scheduler.shutdown() self._event.clear() self._scheduler = None except Exception as e: print(str(e)) def __refresh_rss(self): """ 刷新RSS """ logger.info(f"开始刷新RSS ...") addr_list = self._rss_addrs + [self._douban_address.get(rank) for rank in self._ranks] if not addr_list: logger.info(f"未设置RSS地址") return else: logger.info(f"共 {len(addr_list)} 个RSS地址需要刷新") for addr in addr_list: if not addr: continue try: logger.info(f"获取RSS:{addr} ...") rss_infos = self.__get_rss_info(addr) if not rss_infos: logger.error(f"RSS地址:{addr} ,未查询到数据") continue else: logger.info(f"RSS地址:{addr} ,共 {len(rss_infos)} 条数据") for rss_info in rss_infos: if self._event.is_set(): logger.info(f"订阅服务停止") return title = rss_info.get('title') douban_id = rss_info.get('doubanid') mtype = rss_info.get('type') unique_flag = f"doubanrank: {title} (DB:{douban_id})" # TODO 检查是否已处理过 # TODO 识别媒体信息 # TODO 检查媒体服务器是否存在 # TODO 检查是否已订阅过 # TODO 添加处理历史 # TODO 添加订阅 # TODO 发送通知 # TODO 更新历史记录 except Exception as e: logger.error(str(e)) logger.info(f"所有榜单RSS刷新完成") @staticmethod def __get_rss_info(addr): """ 获取RSS """ try: ret = RequestUtils().get_res(addr) if not ret: return [] ret.encoding = ret.apparent_encoding ret_xml = ret.text ret_array = [] # 解析XML dom_tree = xml.dom.minidom.parseString(ret_xml) rootNode = dom_tree.documentElement items = rootNode.getElementsByTagName("item") for item in items: try: # 标题 title = DomUtils.tag_value(item, "title", default="") # 链接 link = DomUtils.tag_value(item, "link", default="") if not title and not link: logger.warn(f"条目标题和链接均为空,无法处理") continue doubanid = re.findall(r"/(\d+)/", link) if doubanid: doubanid = doubanid[0] if doubanid and not str(doubanid).isdigit(): logger.warn(f"解析的豆瓣ID格式不正确:{doubanid}") continue # 返回对象 ret_array.append({ 'title': title, 'link': link, 'doubanid': doubanid }) except Exception as e1: logger.error("解析RSS条目失败:" + str(e1)) continue return ret_array except Exception as e: logger.error("获取RSS失败:" + str(e)) return []