Files

919 lines
41 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import glob
import os
import shutil
import time
from datetime import datetime, timedelta
from pathlib import Path
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.modules.qbittorrent import Qbittorrent
from app.utils.string import StringUtils
from app.schemas.types import EventType
from app.core.event import eventmanager, Event
from app.core.config import settings
from app.plugins import _PluginBase
from typing import Any, List, Dict, Tuple, Optional
from app.log import logger
from app.schemas import NotificationType
class CleanInvalidSeed(_PluginBase):
# 插件名称
plugin_name = "清理QB无效做种"
# 插件描述
plugin_desc = "清理已经被站点删除的种子及源文件仅支持QB"
# 插件图标
plugin_icon = "clean_a.png"
# 插件版本
plugin_version = "2.2"
# 插件作者
plugin_author = "DzAvril"
# 作者主页
author_url = "https://github.com/DzAvril"
# 插件配置项ID前缀
plugin_config_prefix = "cleaninvalidseed"
# 加载顺序
plugin_order = 1
# 可使用的用户级别
auth_level = 1
# 私有属性
_enabled = False
_cron = None
_notify = False
_onlyonce = False
_qb = None
_detect_invalid_files = False
_delete_invalid_files = False
_delete_invalid_torrents = False
_notify_all = False
_label_only = False
_label = ""
_download_dirs = ""
_exclude_keywords = ""
_exclude_categories = ""
_exclude_labels = ""
_more_logs = False
# 定时器
_scheduler: Optional[BackgroundScheduler] = None
_error_msg = [
"torrent not registered with this tracker",
"Torrent not registered with this tracker",
"torrent banned",
"err torrent banned",
]
_custom_error_msg = ""
def init_plugin(self, config: dict = None):
# 停止现有任务
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._delete_invalid_torrents = config.get("delete_invalid_torrents")
self._delete_invalid_files = config.get("delete_invalid_files")
self._detect_invalid_files = config.get("detect_invalid_files")
self._notify_all = config.get("notify_all")
self._label_only = config.get("label_only")
self._label = config.get("label")
self._download_dirs = config.get("download_dirs")
self._exclude_keywords = config.get("exclude_keywords")
self._exclude_categories = config.get("exclude_categories")
self._exclude_labels = config.get("exclude_labels")
self._custom_error_msg = config.get("custom_error_msg")
self._more_logs = config.get("more_logs")
self._qb = Qbittorrent()
# 加载模块
if self._onlyonce:
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"清理无效种子服务启动,立即运行一次")
self._scheduler.add_job(
func=self.clean_invalid_seed,
trigger="date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
name="清理无效种子",
)
# 关闭一次性开关
self._onlyonce = False
self._update_config()
# 启动任务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
self._scheduler.start()
def get_state(self) -> bool:
return self._enabled
def _update_config(self):
self.update_config(
{
"onlyonce": False,
"cron": self._cron,
"enabled": self._enabled,
"notify": self._notify,
"delete_invalid_torrents": self._delete_invalid_torrents,
"delete_invalid_files": self._delete_invalid_files,
"detect_invalid_files": self._detect_invalid_files,
"notify_all": self._notify_all,
"label_only": self._label_only,
"label": self._label,
"download_dirs": self._download_dirs,
"exclude_keywords": self._exclude_keywords,
"exclude_categories": self._exclude_categories,
"exclude_labels": self._exclude_labels,
"custom_error_msg": self._custom_error_msg,
"more_logs": self._more_logs,
}
)
@staticmethod
def get_command() -> List[Dict[str, Any]]:
"""
定义远程控制命令
:return: 命令关键字、事件、描述、附带数据
"""
return [
{
"cmd": "/detect_invalid_torrents",
"event": EventType.PluginAction,
"desc": "检测无效做种",
"category": "QB",
"data": {"action": "detect_invalid_torrents"},
},
{
"cmd": "/delete_invalid_torrents",
"event": EventType.PluginAction,
"desc": "清理无效做种",
"category": "QB",
"data": {"action": "delete_invalid_torrents"},
},
{
"cmd": "/detect_invalid_files",
"event": EventType.PluginAction,
"desc": "检测无效源文件",
"category": "QB",
"data": {"action": "detect_invalid_files"},
},
{
"cmd": "/delete_invalid_files",
"event": EventType.PluginAction,
"desc": "清理无效源文件",
"category": "QB",
"data": {"action": "delete_invalid_files"},
},
{
"cmd": "/toggle_notify_all",
"event": EventType.PluginAction,
"desc": "QB清理插件切换全量通知",
"category": "QB",
"data": {"action": "toggle_notify_all"},
},
]
@eventmanager.register(EventType.PluginAction)
def handle_commands(self, event: Event):
if event:
event_data = event.event_data
if event_data:
if not (
event_data.get("action") == "detect_invalid_torrents"
or event_data.get("action") == "delete_invalid_torrents"
or event_data.get("action") == "detect_invalid_files"
or event_data.get("action") == "delete_invalid_files"
or event_data.get("action") == "toggle_notify_all"
):
return
self.post_message(
channel=event.event_data.get("channel"),
title="开始执行远程命令...",
userid=event.event_data.get("user"),
)
old_delete_invalid_torrents = self._delete_invalid_torrents
old_detect_invalid_files = self._detect_invalid_files
old_delete_invalid_files = self._delete_invalid_files
if event_data.get("action") == "detect_invalid_torrents":
logger.info("收到远程命令,开始检测无效做种")
self._delete_invalid_torrents = False
self._detect_invalid_files = False
self._delete_invalid_files = False
self.clean_invalid_seed()
elif event_data.get("action") == "delete_invalid_torrents":
logger.info("收到远程命令,开始清理无效做种")
self._delete_invalid_torrents = True
self._detect_invalid_files = False
self._delete_invalid_files = False
self.clean_invalid_seed()
elif event_data.get("action") == "detect_invalid_files":
logger.info("收到远程命令,开始检测无效源文件")
self._delete_invalid_files = False
self.detect_invalid_files()
elif event_data.get("action") == "delete_invalid_files":
logger.info("收到远程命令,开始清理无效源文件")
self._delete_invalid_files = True
self.detect_invalid_files()
elif event_data.get("action") == "toggle_notify_all":
self._notify_all = not self._notify_all
self._update_config()
if self._notify_all:
self.post_message(
channel=event.event_data.get("channel"),
title="已开启全量通知",
userid=event.event_data.get("user"),
)
else:
self.post_message(
channel=event.event_data.get("channel"),
title="已关闭全量通知",
userid=event.event_data.get("user"),
)
return
else:
logger.error("收到未知远程命令")
return
self._delete_invalid_torrents = old_delete_invalid_torrents
self._detect_invalid_files = old_detect_invalid_files
self._delete_invalid_files = old_delete_invalid_files
self.post_message(
channel=event.event_data.get("channel"),
title="远程命令执行完成!",
userid=event.event_data.get("user"),
)
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_service(self) -> List[Dict[str, Any]]:
"""
注册插件公共服务
[{
"id": "服务ID",
"name": "服务名称",
"trigger": "触发器cron/interval/date/CronTrigger.from_crontab()",
"func": self.xxx,
"kwargs": {} # 定时器参数
}]
"""
if self._enabled and self._cron:
return [
{
"id": "CleanInvalidSeed",
"name": "清理QB无效做种",
"trigger": CronTrigger.from_crontab(self._cron),
"func": self.clean_invalid_seed,
"kwargs": {},
}
]
def get_all_torrents(self):
all_torrents, error = self._qb.get_torrents()
if error:
logger.error(f"获取QB种子失败: {error}")
if self._notify:
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理QB无效做种】",
text=f"获取QB种子失败请检查QB配置",
)
return []
if not all_torrents:
logger.warning("QB没有种子")
if self._notify:
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理QB无效做种】",
text=f"QB中没有种子",
)
return []
return all_torrents
def clean_invalid_seed(self):
logger.info("开始清理QB无效做种")
all_torrents = self.get_all_torrents()
temp_invalid_torrents = []
# tracker未工作但暂时不能判定为失效做种需人工判断
tracker_not_working_torrents = []
working_tracker_set = set()
exclude_categories = (
self._exclude_categories.split("\n") if self._exclude_categories else []
)
exclude_labels = (
self._exclude_labels.split("\n") if self._exclude_labels else []
)
custom_msgs = (
self._custom_error_msg.split("\n") if self._custom_error_msg else []
)
error_msgs = self._error_msg + custom_msgs
# 第一轮筛选出所有未工作的种子
for torrent in all_torrents:
trackers = torrent.trackers
is_invalid = True
is_tracker_working = False
for tracker in trackers:
if tracker.get("tier") == -1:
continue
tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1]
# 有一个tracker工作即为有效做种
if (tracker.get("status") == 2) or (tracker.get("status") == 3):
is_tracker_working = True
if not (
(tracker.get("status") == 4) and (tracker.get("msg") in error_msgs)
):
is_invalid = False
working_tracker_set.add(tracker_domian)
if self._more_logs:
logger.info(f"处理 [{torrent.name}] tracker [{tracker_domian}]: 分类: [{torrent.category}], 标签: [{torrent.tags}], 状态: [{tracker.get('status')}], msg: [{tracker.get('msg')}], is_invalid: [{is_invalid}], is_working: [{is_tracker_working}]")
if is_invalid:
temp_invalid_torrents.append(torrent)
elif not is_tracker_working:
# 排除已暂停的种子
if not torrent.state_enum.is_paused:
tracker_not_working_torrents.append(torrent)
logger.info(f"初筛共有{len(temp_invalid_torrents)}个无效做种")
# 第二轮筛选出tracker有正常工作种子而当前种子未工作的避免因临时关站或tracker失效导致误删的问题
# 失效做种但通过种子分类排除的种子
invalid_torrents_exclude_categories = []
# 失效做种但通过种子标签排除的种子
invalid_torrents_exclude_labels = []
# 将invalid_torrents基本信息保存起来在种子被删除后依然可以打印这些信息
invalid_torrent_tuple_list = []
deleted_torrent_tuple_list = []
for torrent in temp_invalid_torrents:
trackers = torrent.trackers
for tracker in trackers:
if tracker.get("tier") == -1:
continue
tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1]
if tracker_domian in working_tracker_set:
# tracker是正常的说明该种子是无效的
invalid_torrent_tuple_list.append(
(
torrent.name,
torrent.category,
torrent.tags,
torrent.size,
tracker_domian,
tracker.msg,
)
)
if self._delete_invalid_torrents or self._label_only:
# 检查种子分类和标签是否排除
is_excluded = False
if torrent.category in exclude_categories:
is_excluded = True
invalid_torrents_exclude_categories.append(torrent)
torrent_labels = [
tag.strip() for tag in torrent.tags.split(",")
]
for label in torrent_labels:
if label in exclude_labels:
is_excluded = True
invalid_torrents_exclude_labels.append(torrent)
if not is_excluded:
if self._label_only:
# 仅标记
self._qb.set_torrents_tag(ids=torrent.get("hash"), tags=[self._label if self._label != "" else "无效做种"])
else:
# 只删除种子不删除文件,以防其它站点辅种
self._qb.delete_torrents(False, torrent.get("hash"))
# 标记已处理种子信息
deleted_torrent_tuple_list.append(
(
torrent.name,
torrent.category,
torrent.tags,
torrent.size,
tracker_domian,
tracker.msg,
)
)
break
invalid_msg = f"检测到{len(invalid_torrent_tuple_list)}个失效做种\n"
tracker_not_working_msg = f"检测到{len(tracker_not_working_torrents)}个tracker未工作做种请检查种子状态\n"
if self._label_only or self._delete_invalid_torrents:
if self._label_only:
deleted_msg = f"标记了{len(deleted_torrent_tuple_list)}个失效种子\n"
else:
deleted_msg = f"删除了{len(deleted_torrent_tuple_list)}个失效种子\n"
if len(exclude_categories) != 0:
exclude_categories_msg = f"分类排除{len(invalid_torrents_exclude_categories)}个失效种子未删除,请手动处理\n"
if len(exclude_labels) != 0:
exclude_labels_msg = f"标签排除{len(invalid_torrents_exclude_labels)}个失效种子未删除,请手动处理\n"
for index in range(len(invalid_torrent_tuple_list)):
torrent = invalid_torrent_tuple_list[index]
invalid_msg += f"{index + 1}. {torrent[0]},分类:{torrent[1]},标签:{torrent[2]}, 大小:{StringUtils.str_filesize(torrent[3])}Trackers: {torrent[4]}{torrent[5]}\n"
for index in range(len(tracker_not_working_torrents)):
torrent = tracker_not_working_torrents[index]
trackers = torrent.trackers
tracker_msg = ""
for tracker in trackers:
if tracker.get("tier") == -1:
continue
tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1]
tracker_msg += f" {tracker_domian}{tracker.msg} "
tracker_not_working_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)}Trackers: {tracker_msg}\n"
for index in range(len(invalid_torrents_exclude_categories)):
torrent = invalid_torrents_exclude_categories[index]
trackers = torrent.trackers
tracker_msg = ""
for tracker in trackers:
if tracker.get("tier") == -1:
continue
tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1]
tracker_msg += f" {tracker_domian}{tracker.msg} "
exclude_categories_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)}Trackers: {tracker_msg}\n"
for index in range(len(invalid_torrents_exclude_labels)):
torrent = invalid_torrents_exclude_labels[index]
trackers = torrent.trackers
tracker_msg = ""
for tracker in trackers:
if tracker.get("tier") == -1:
continue
tracker_domian = StringUtils.get_url_netloc((tracker.get("url")))[1]
tracker_msg += f" {tracker_domian}{tracker.msg} "
exclude_labels_msg += f"{index + 1}. {torrent.name},分类:{torrent.category},标签:{torrent.tags}, 大小:{StringUtils.str_filesize(torrent.size)}Trackers: {tracker_msg}\n"
for index in range(len(deleted_torrent_tuple_list)):
torrent = deleted_torrent_tuple_list[index]
deleted_msg += f"{index + 1}. {torrent[0]},分类:{torrent[1]},标签:{torrent[2]}, 大小:{StringUtils.str_filesize(torrent[3])}Trackers: {torrent[4]}{torrent[5]}\n"
# 日志
logger.info(invalid_msg)
logger.info(tracker_not_working_msg)
if self._delete_invalid_torrents:
logger.info(deleted_msg)
if len(exclude_categories) != 0:
logger.info(exclude_categories_msg)
if len(exclude_labels) != 0:
logger.info(exclude_labels_msg)
# 通知
if self._notify:
invalid_msg = invalid_msg.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=invalid_msg,
)
if self._notify_all:
tracker_not_working_msg = tracker_not_working_msg.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=tracker_not_working_msg,
)
if self._label_only or self._delete_invalid_torrents:
deleted_msg = deleted_msg.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=deleted_msg,
)
if self._notify_all:
exclude_categories_msg = exclude_categories_msg.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=exclude_categories_msg,
)
exclude_labels_msg = exclude_labels_msg.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=exclude_labels_msg,
)
logger.info("检测无效做种任务结束")
if self._detect_invalid_files:
self.detect_invalid_files()
def detect_invalid_files(self):
logger.info("开始检测未做种的无效源文件")
all_torrents = self.get_all_torrents()
source_path_map = {}
source_paths = []
total_size = 0
deleted_file_cnt = 0
exclude_key_words = (
self._exclude_keywords.split("\n") if self._exclude_keywords else []
)
if not self._download_dirs:
logger.error("未配置下载目录,无法检测未做种无效源文件")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【检测无效源文件】",
text="未配置下载目录,无法检测未做种无效源文件",
)
return
for path in self._download_dirs.split("\n"):
mp_path, qb_path = path.split(":")
source_path_map[mp_path] = qb_path
source_paths.append(mp_path)
# 所有做种源文件路径
content_path_set = set()
for torrent in all_torrents:
content_path_set.add(torrent.content_path)
message = "检测未做种无效源文件:\n"
for source_path_str in source_paths:
source_path = Path(source_path_str)
# 判断source_path是否存在
if not source_path.exists():
logger.error(f"{source_path} 不存在,无法检测未做种无效源文件")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【检测无效源文件】",
text=f"{source_path} 不存在,无法检测未做种无效源文件",
)
continue
source_files = []
# 获取source_path下的所有文件包括文件夹
for file in source_path.iterdir():
source_files.append(file)
for source_file in source_files:
skip = False
for key_word in exclude_key_words:
if key_word in source_file.name:
logger.info(f"{str(source_file)}命中关键字{key_word},不做处理")
skip = True
break
if skip:
continue
# 将mp_path替换成 qb_path
qb_path = (str(source_file)).replace(
source_path_str, source_path_map[source_path_str]
)
# todo: 优化性能
is_exist = False
for content_path in content_path_set:
if qb_path in content_path:
is_exist = True
break
if not is_exist:
deleted_file_cnt += 1
message += f"{deleted_file_cnt}. {str(source_file)}\n"
total_size += self.get_size(source_file)
if self._delete_invalid_files:
if source_file.is_file():
source_file.unlink()
elif source_file.is_dir():
shutil.rmtree(source_file)
message += f"检测到{deleted_file_cnt}个未做种的无效源文件,共占用{StringUtils.str_filesize(total_size)}空间。\n"
if self._delete_invalid_files:
message += f"***已删除无效源文件,释放{StringUtils.str_filesize(total_size)}空间!***\n"
logger.info(message)
if self._notify:
message = message.replace("_", "\_")
self.post_message(
mtype=NotificationType.SiteMessage,
title=f"【清理无效做种】",
text=message,
)
logger.info("检测无效源文件任务结束")
def get_size(self, path: Path):
total_size = 0
if path.is_file():
return path.stat().st_size
# rglob 方法用于递归遍历所有文件和目录
for entry in path.rglob("*"):
if entry.is_file():
total_size += entry.stat().st_size
return total_size
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
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": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "delete_invalid_torrents",
"label": "删除无效种子(确认无误后再开启)",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "detect_invalid_files",
"label": "检测无效源文件",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "delete_invalid_files",
"label": "删除无效源文件(确认无误后再开启)",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "notify_all",
"label": "全量通知",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "label_only",
"label": "仅标记模式(开启后不会删种)",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VSwitch",
"props": {
"model": "more_logs",
"label": "打印更多日志",
},
}
],
},
],
},
{
"component": "VRow",
"content": [
{
"component": "VCol",
"props": { "cols": 12, "md": 6 },
"content": [
{
"component": "VTextField",
"props": {
"model": "cron",
"label": "执行周期",
},
}
],
},
{
"component": "VCol",
"props": { "cols": 12, "md": 6 },
"content": [
{
"component": "VTextField",
"props": {
"model": "label",
"label": "增加标签",
"placeholder": "仅标记模式下生效,给待处理的种子打标签",
},
}
],
},
],
},
{
"component": "VRow",
"content": [
{
"component": "VCol",
"props": {"cols": 12},
"content": [
{
"component": "VTextarea",
"props": {
"model": "download_dirs",
"label": "下载目录映射",
"rows": 2,
"placeholder": "填写要监控的源文件目录并设置MP和QB的目录映射关系如/mp/download:/qb/download多个目录请换行",
},
}
],
}
],
},
{
"component": "VRow",
"props": {"style": {"margin-top": "0px"}},
"content": [
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VTextarea",
"props": {
"model": "exclude_keywords",
"label": "过滤删源文件关键字",
"rows": 2,
"placeholder": "多个关键字请换行,仅针对删除源文件",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VTextarea",
"props": {
"model": "exclude_categories",
"label": "过滤删种分类",
"rows": 2,
"placeholder": "多个分类请换行,仅针对删除种子",
},
}
],
},
{
"component": "VCol",
"props": {"cols": 12, "md": 4},
"content": [
{
"component": "VTextarea",
"props": {
"model": "exclude_labels",
"label": "过滤删种标签",
"rows": 2,
"placeholder": "多个标签请换行,仅针对删除种子",
},
}
],
},
],
},
{
"component": "VRow",
"content": [
{
"component": "VCol",
"props": {"cols": 12},
"content": [
{
"component": "VTextarea",
"props": {
"model": "custom_error_msg",
"label": "自定义无效做种tracker错误信息",
"rows": 5,
"placeholder": "填入想要清理的种子的tracker错误信息'skipping tracker announce (unreachable)',多个信息请换行",
},
}
],
}
],
},
{
"component": "VRow",
"content": [
{
"component": "VCol",
"props": {
"cols": 12,
},
"content": [
{
"component": "VAlert",
"props": {
"type": "error",
"variant": "tonal",
"text": "谨慎起见删除种子/源文件功能做了开关,请确认无误后再开启删除功能",
},
}
],
},
{
"component": "VCol",
"props": {
"cols": 12,
},
"content": [
{
"component": "VAlert",
"props": {
"type": "info",
"variant": "tonal",
"text": "下载目录映射填入源文件根目录并设置MP和QB的目录映射关系。如某种子下载的源文件A存放路径为/qb/download/A则目录映射填入/mp/download:/qb/download多个目录请换行。注意映射目录不要有多余的'/'",
},
}
],
},
],
},
],
}
], {
"enabled": False,
"notify": False,
"download_dirs": "",
"delete_invalid_torrents": False,
"delete_invalid_files": False,
"detect_invalid_files": False,
"notify_all": False,
"onlyonce": False,
"cron": "0 0 * * *",
"label_only": False,
"label": "",
"more_logs": False,
}
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._scheduler.shutdown()
self._scheduler = None
except Exception as e:
logger.error("退出插件失败:%s" % str(e))