diff --git a/app/plugins/chinesesubfinder/__init__.py b/app/plugins/chinesesubfinder/__init__.py new file mode 100644 index 00000000..a0ae9da1 --- /dev/null +++ b/app/plugins/chinesesubfinder/__init__.py @@ -0,0 +1,154 @@ +from functools import lru_cache +from pathlib import Path +from typing import List, Tuple, Dict, Any + +from app.core.config import settings +from app.core.event import eventmanager +from app.log import logger +from app.plugins import _PluginBase +from app.schemas.types import EventType, MediaType +from app.utils.http import RequestUtils + + +class ChineseSubFinder(_PluginBase): + # 插件名称 + plugin_name = "ChineseSubFinder" + # 插件描述 + plugin_desc = "通知ChineseSubFinder下载字幕。" + # 插件图标 + plugin_icon = "chinesesubfinder.png" + # 主题色 + plugin_color = "#83BE39" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "chinesesubfinder_" + # 加载顺序 + plugin_order = 3 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _save_tmp_path = None + _enable = False + _host = None + _api_key = None + _remote_path = None + _local_path = None + _remote_path2 = None + _local_path2 = None + _remote_path3 = None + _local_path3 = None + + def init_plugin(self, config: dict = None): + self._save_tmp_path = settings.TEMP_PATH + if config: + self._enable = config.get("enable") + self._api_key = config.get("api_key") + self._host = config.get('host') + if self._host: + if not self._host.startswith('http'): + self._host = "http://" + self._host + if not self._host.endswith('/'): + self._host = self._host + "/" + self._local_path = config.get("local_path") + self._remote_path = config.get("remote_path") + self._local_path2 = config.get("local_path2") + self._remote_path2 = config.get("remote_path2") + self._local_path3 = config.get("local_path3") + self._remote_path3 = config.get("remote_path3") + + @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): + pass + + @eventmanager.register(EventType.TransferComplete) + def download(self, event): + """ + 调用ChineseSubFinder下载字幕 + """ + if not self._host or not self._api_key: + return + item = event.event_data + if not item: + return + # FIXME + req_url = "%sapi/v1/add-job" % self._host + + item_media = item.get("media_info") + item_type = item_media.get("type") + item_bluray = item.get("bluray") + item_file = item.get("file") + item_file_ext = item.get("file_ext") + + if item_bluray: + file_path = "%s.mp4" % item_file + else: + if Path(item_file).suffix != item_file_ext: + file_path = "%s%s" % (item_file, item_file_ext) + else: + file_path = item_file + + # 路径替换 + if self._local_path and self._remote_path and file_path.startswith(self._local_path): + file_path = file_path.replace(self._local_path, self._remote_path).replace('\\', '/') + + if self._local_path2 and self._remote_path2 and file_path.startswith(self._local_path2): + file_path = file_path.replace(self._local_path2, self._remote_path2).replace('\\', '/') + + if self._local_path3 and self._remote_path3 and file_path.startswith(self._local_path3): + file_path = file_path.replace(self._local_path3, self._remote_path3).replace('\\', '/') + + # 调用CSF下载字幕 + self.__request_csf(req_url=req_url, + file_path=file_path, + item_type=0 if item_type == MediaType.MOVIE.value else 1, + item_bluray=item_bluray) + + @lru_cache(maxsize=128) + def __request_csf(self, req_url, file_path, item_type, item_bluray): + # 一个名称只建一个任务 + logger.info("通知ChineseSubFinder下载字幕: %s" % file_path) + params = { + "video_type": item_type, + "physical_video_file_full_path": file_path, + "task_priority_level": 3, + "media_server_inside_video_id": "", + "is_bluray": item_bluray + } + try: + res = RequestUtils(headers={ + "Authorization": "Bearer %s" % self._api_key + }).post(req_url, json=params) + if not res or res.status_code != 200: + logger.error("调用ChineseSubFinder API失败!") + else: + # 如果文件目录没有识别的nfo元数据, 此接口会返回控制符,推测是ChineseSubFinder的原因 + # emby refresh元数据时异步的 + if res.text: + job_id = res.json().get("job_id") + message = res.json().get("message") + if not job_id: + logger.warn("ChineseSubFinder下载字幕出错:%s" % message) + else: + logger.info("ChineseSubFinder任务添加成功:%s" % job_id) + else: + logger.error("%s 目录缺失nfo元数据" % file_path) + except Exception as e: + logger.error("连接ChineseSubFinder出错:" + str(e)) diff --git a/app/plugins/customhosts/__init__.py b/app/plugins/customhosts/__init__.py new file mode 100644 index 00000000..084b06e2 --- /dev/null +++ b/app/plugins/customhosts/__init__.py @@ -0,0 +1,143 @@ +from typing import List, Tuple, Dict, Any + +from app.core.event import eventmanager +from app.log import logger +from app.plugins import _PluginBase +from app.schemas.types import EventType +from app.utils.ip import IpUtils +from app.utils.system import SystemUtils + +from python_hosts import Hosts, HostsEntry + + +class CustomHosts(_PluginBase): + # 插件名称 + plugin_name = "自定义Hosts" + # 插件描述 + plugin_desc = "修改系统hosts文件,加速网络访问。" + # 插件图标 + plugin_icon = "hosts.png" + # 主题色 + plugin_color = "#02C4E0" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "thsrite" + # 作者主页 + author_url = "https://github.com/thsrite" + # 插件配置项ID前缀 + plugin_config_prefix = "customhosts_" + # 加载顺序 + plugin_order = 11 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _hosts = [] + _enable = False + + def init_plugin(self, config: dict = None): + # 读取配置 + if config: + self._enable = config.get("enable") + self._hosts = config.get("hosts") + if isinstance(self._hosts, str): + self._hosts = str(self._hosts).split('\n') + if self._enable and self._hosts: + # 排除空的host + new_hosts = [] + for host in self._hosts: + if host and host != '\n': + new_hosts.append(host.replace("\n", "") + "\n") + self._hosts = new_hosts + + # 添加到系统 + error_flag, error_hosts = self.__add_hosts_to_system(self._hosts) + self._enable = self._enable and not error_flag + + # 更新错误Hosts + self.update_config({ + "hosts": self._hosts, + "err_hosts": error_hosts, + "enable": self._enable + }) + + @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 + + @staticmethod + def __read_system_hosts(): + """ + 读取系统hosts对象 + """ + # 获取本机hosts路径 + if SystemUtils.is_windows(): + hosts_path = r"c:\windows\system32\drivers\etc\hosts" + else: + hosts_path = '/etc/hosts' + # 读取系统hosts + return Hosts(path=hosts_path) + + def __add_hosts_to_system(self, hosts): + """ + 添加hosts到系统 + """ + # 系统hosts对象 + system_hosts = self.__read_system_hosts() + # 过滤掉插件添加的hosts + orgin_entries = [] + for entry in system_hosts.entries: + if entry.entry_type == "comment" and entry.comment == "# CustomHostsPlugin": + break + orgin_entries.append(entry) + system_hosts.entries = orgin_entries + # 新的有效hosts + new_entrys = [] + # 新的错误的hosts + err_hosts = [] + err_flag = False + for host in hosts: + if not host: + continue + host_arr = str(host).split() + try: + host_entry = HostsEntry(entry_type='ipv4' if IpUtils.is_ipv4(str(host_arr[0])) else 'ipv6', + address=host_arr[0], + names=host_arr[1:]) + new_entrys.append(host_entry) + except Exception as err: + err_hosts.append(host + "\n") + logger.error(f"{host} 格式转换错误:{str(err)}") + + # 写入系统hosts + if new_entrys: + try: + # 添加分隔标识 + system_hosts.add([HostsEntry(entry_type='comment', comment="# CustomHostsPlugin")]) + # 添加新的Hosts + system_hosts.add(new_entrys) + system_hosts.write() + logger.info("更新系统hosts文件成功") + except Exception as err: + err_flag = True + logger.error(f"更新系统hosts文件失败:{str(err) or '请检查权限'}") + return err_flag, err_hosts + + def get_state(self): + return self._enable and self._hosts and self._hosts[0] + + def stop_service(self): + """ + 退出插件 + """ + pass diff --git a/app/plugins/dirmonitor/__init__.py b/app/plugins/dirmonitor/__init__.py new file mode 100644 index 00000000..24efaec9 --- /dev/null +++ b/app/plugins/dirmonitor/__init__.py @@ -0,0 +1,59 @@ +from typing import List, Tuple, Dict, Any + +from app.plugins import _PluginBase + + +class DirMonitor(_PluginBase): + # 插件名称 + plugin_name = "目录监控" + # 插件描述 + plugin_desc = "监控目录,文件发生变化时实时整理到媒体库。" + # 插件图标 + plugin_icon = "synctimer.png" + # 主题色 + plugin_color = "#53BA48" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "dirmonitor_" + # 加载顺序 + plugin_order = 5 + # 可使用的用户级别 + user_level = 1 + + # 私有属性 + _monitor = None + _enable = False + + def init_plugin(self, config: dict = None): + # 读取配置 + if config: + self._enable = config.get("enable") + + # 停止现有任务 + self.stop_service() + + # TODO 启动任务 + + @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): + """ + 退出插件 + """ + pass diff --git a/app/plugins/doubanrank/__init__.py b/app/plugins/doubanrank/__init__.py new file mode 100644 index 00000000..668b7fab --- /dev/null +++ b/app/plugins/doubanrank/__init__.py @@ -0,0 +1,210 @@ +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 [] diff --git a/app/plugins/torrentremover/__init__.py b/app/plugins/torrentremover/__init__.py new file mode 100644 index 00000000..0194727d --- /dev/null +++ b/app/plugins/torrentremover/__init__.py @@ -0,0 +1,65 @@ +from typing import List, Tuple, Dict, Any + +from app.core.event import eventmanager +from app.plugins import _PluginBase +from app.schemas.types import EventType + + +class TorrentRemover(_PluginBase): + # 插件名称 + plugin_name = "下载任务联动删除" + # 插件描述 + plugin_desc = "历史记录被删除时,同步删除下载器中的下载任务。" + # 插件图标 + plugin_icon = "torrentremover.png" + # 主题色 + plugin_color = "#F44336" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "torrentremover_" + # 加载顺序 + plugin_order = 9 + # 可使用的用户级别 + auth_level = 2 + + # 私有属性 + downloader = None + _enable = False + + def init_plugin(self, config: dict = None): + if config: + self._enable = config.get("enable") + + @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): + pass + + @eventmanager.register(EventType.HistoryDeleted) + def deletetorrent(self, event): + """ + 联动删除下载器中的下载任务 + """ + if not self._enable: + return + event_info = event.event_data + if not event_info: + return + + # TODO 删除所有下载任务 diff --git a/app/schemas/types.py b/app/schemas/types.py index 33b09201..cf94e3ca 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -30,6 +30,8 @@ class EventType(Enum): TransferComplete = "transfer.complete" # 添加下载 DownloadAdded = "download.added" + # 删除历史记录 + HistoryDeleted = "history.deleted" # 系统配置Key字典 diff --git a/requirements.txt b/requirements.txt index bc5abaff..ab2774be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,4 +42,5 @@ starlette~=0.27.0 PyVirtualDisplay~=3.0 Cython~=0.29.35 tvdb_api~=3.1 -psutil==5.9.4 \ No newline at end of file +psutil~=5.9.4 +python_hosts~=1.0.3 \ No newline at end of file