from app.modules.qbittorrent import Qbittorrent from app.modules.transmission import Transmission from app.plugins import _PluginBase from typing import Any, List, Dict, Tuple from app.log import logger class RemoveTorrent(_PluginBase): # 插件名称 plugin_name = "删除站点种子" # 插件描述 plugin_desc = "删除下载器中某站点种子。" # 插件图标 plugin_icon = "delete.png" # 插件版本 plugin_version = "1.2" # 插件作者 plugin_author = "thsrite" # 作者主页 author_url = "https://github.com/thsrite" # 插件配置项ID前缀 plugin_config_prefix = "removetorrent_" # 加载顺序 plugin_order = 30 # 可使用的用户级别 auth_level = 2 # 私有属性 _downloader = None _onlyonce = None _delete_type = False _delete_torrent = False _delete_file = False _trackers = None qb = None tr = None def init_plugin(self, config: dict = None): self.qb = Qbittorrent() self.tr = Transmission() if config: self._downloader = config.get("downloader") self._onlyonce = config.get("onlyonce") self._delete_type = config.get("delete_type") self._delete_torrent = config.get("delete_torrent") self._delete_file = config.get("delete_file") self._trackers = config.get("trackers") if self._trackers and self._onlyonce: self.update_config({ "downloader": self._downloader, "delete_type": self._delete_type, "delete_torrent": self._delete_torrent, "delete_file": self._delete_file, "trackers": self._trackers, "onlyonce": False }) for tracker in str(self._trackers).split("\n"): logger.info(f"下载器 {self._downloader} 开始处理站点tracker {tracker}") self.__check_feed(tracker) logger.info(f"下载器 {self._downloader} 处理站点tracker {tracker} 完成") def __check_feed(self, tracker: str): """ 检查tracker辅种情况 """ downloader_obj = self.__get_downloader(self._downloader) # 获取下载器中已完成的种子 torrents = downloader_obj.get_completed_torrents() if not torrents: logger.info(f"下载器 {self._downloader} 未获取到已完成种子") return logger.info(f"下载器 {self._downloader} 获取到已完成种子 {len(torrents)} 个") all_torrents = [] tracker_torrents = [] key_torrents = {} # 遍历种子,以种子名称和种子大小为key,查询辅种数量 for torrent in torrents: torrent_size = self.__get_torrent_size(torrent, self._downloader) torrent_name = self.__get_torrent_name(torrent, self._downloader) torrent_key = "%s-%s" % (torrent_name, torrent_size) all_torrents.append(torrent_key) torrent_trackers = self.__get_torrent_trackers(torrent, self._downloader) if str(self._downloader) == "qb": # 命中tracker的种子 if str(tracker) in torrent_trackers: tracker_torrents.append(torrent_key) key_torrents[torrent_key] = torrent else: for torrent_tracker in torrent_trackers: # 命中tracker的种子 if str(tracker) in torrent_tracker.get('announce'): tracker_torrents.append(torrent_key) key_torrents[torrent_key] = torrent if not tracker_torrents: logger.error(f"下载器 {self._downloader} 未获取到命中tracker {tracker} 的种子") return logger.info(f"下载器 {self._downloader} 获取到命中tracker {tracker} 已完成种子 {len(tracker_torrents)} 个") # 查询tracker种子是否有其他辅种 for tracker_torrent in tracker_torrents: torrent = key_torrents.get(tracker_torrent) torrent_name = self.__get_torrent_name(torrent, self._downloader) torrent_hash = self.__get_torrent_hash(torrent, self._downloader) if self._delete_type: # 有辅种 if all_torrents.count(tracker_torrent) > 1: # 删除逻辑 if self._delete_torrent: downloader_obj.delete_torrents(delete_file=self._delete_file, ids=torrent_hash) logger.info(f"种子 {torrent_name} {torrent_hash} 有其他辅种,已删除") else: logger.info(f"种子 {torrent_name} {torrent_hash} 有其他辅种,可删除") else: # 无辅种 logger.warn(f"种子 {torrent_name} {torrent_hash} 在其他站无辅种,如需删除请手动处理") else: # 无辅种 if all_torrents.count(tracker_torrent) == 1: # 删除逻辑 if self._delete_torrent: downloader_obj.delete_torrents(delete_file=self._delete_file, ids=torrent_hash) logger.info(f"种子 {torrent_name} {torrent_hash} 无其他辅种,已删除") else: logger.info(f"种子 {torrent_name} {torrent_hash} 无其他辅种,可删除") else: logger.warn(f"种子 {torrent_name} {torrent_hash} 在其他站有辅种,如需删除请手动处理") def __get_downloader(self, dtype: str): """ 根据类型返回下载器实例 """ if dtype == "qb": return self.qb elif dtype == "tr": return self.tr else: return None @staticmethod def __get_torrent_trackers(torrent: Any, dl_type: str): """ 获取种子trackers """ try: return torrent.get("tracker") if dl_type == "qb" else torrent.trackers except Exception as e: print(str(e)) return "" @staticmethod def __get_torrent_name(torrent: Any, dl_type: str): """ 获取种子name """ try: return torrent.get("name") if dl_type == "qb" else torrent.name except Exception as e: print(str(e)) return "" @staticmethod def __get_torrent_size(torrent: Any, dl_type: str): """ 获取种子大小 """ try: return torrent.get("size") if dl_type == "qb" else torrent.total_size except Exception as e: print(str(e)) return "" @staticmethod def __get_torrent_hash(torrent: Any, dl_type: str): """ 获取种子hash """ try: return torrent.get("hash") if dl_type == "qb" else torrent.hashString except Exception as e: print(str(e)) return "" def get_state(self) -> bool: return False @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]]: """ 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 """ return [ { 'component': 'VForm', 'content': [ { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, 'md': 3 }, 'content': [ { 'component': 'VSwitch', 'props': { 'model': 'onlyonce', 'label': '立即运行一次', } } ] }, ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, 'md': 3 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'downloader', 'label': '下载器', 'items': [ {'title': 'qb', 'value': 'qb'}, {'title': 'tr', 'value': 'tr'} ] } } ] }, { 'component': 'VCol', 'props': { 'cols': 12, 'md': 3 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'delete_type', 'label': '是否有辅种', 'items': [ {'title': '是', 'value': True}, {'title': '否', 'value': False} ] } } ] }, { 'component': 'VCol', 'props': { 'cols': 12, 'md': 3 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'delete_torrent', 'label': '删除种子', 'items': [ {'title': '是', 'value': True}, {'title': '否', 'value': False} ] } } ] }, { 'component': 'VCol', 'props': { 'cols': 12, 'md': 3 }, 'content': [ { 'component': 'VSelect', 'props': { 'model': 'delete_file', 'label': '删除文件', 'items': [ {'title': '是', 'value': True}, {'title': '否', 'value': False} ] } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, }, 'content': [ { 'component': 'VTextarea', 'props': { 'model': 'trackers', 'rows': '3', 'label': '站点tracker域名', 'placeholder': '站点tracker域名,一行一个' } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, }, 'content': [ { 'component': 'VAlert', 'props': { 'type': 'info', 'variant': 'tonal', 'text': '输入要删除辅种的站点tracker域名。' '保留站点没有辅种的种子,其余在其他站有辅种的种子均删除。' '(适用于某个站点不想保种了,但是可能有孤种没法直接全部删除的情况)' } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, }, 'content': [ { 'component': 'VAlert', 'props': { 'type': 'info', 'variant': 'tonal', 'text': '场景一:某个站不想保种了,但是有些种子没有辅种,需要保留。' '是否有辅种=是,删除种子=是,删除文件=否。' '(保留站点没有辅种的种子,其余在其他站有辅种的种子均删除(保留文件)。)' } } ] } ] }, { 'component': 'VRow', 'content': [ { 'component': 'VCol', 'props': { 'cols': 12, }, 'content': [ { 'component': 'VAlert', 'props': { 'type': 'info', 'variant': 'tonal', 'text': '场景二:想删除某个站没有辅种的种子。' '是否有辅种=否,删除种子=是,删除文件=是。' } } ] } ] } ] } ], { "downloader": "qb", "delete_type": True, "delete_torrent": False, "delete_file": False, "onlyonce": False, "trackers": "" } def get_page(self) -> List[dict]: pass def stop_service(self): """ 退出插件 """ pass