mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-03-27 10:05:57 +00:00
1009 lines
45 KiB
Python
1009 lines
45 KiB
Python
import shutil
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
from typing import Any, List, Dict, Tuple, Optional
|
||
|
||
import pytz
|
||
from apscheduler.schedulers.background import BackgroundScheduler
|
||
from apscheduler.triggers.cron import CronTrigger
|
||
|
||
from app.core.config import settings
|
||
from app.core.event import eventmanager, Event
|
||
from app.helper.downloader import DownloaderHelper
|
||
from app.log import logger
|
||
from app.plugins import _PluginBase
|
||
from app.schemas import NotificationType
|
||
from app.schemas import ServiceInfo
|
||
from app.schemas.types import EventType
|
||
from app.utils.string import StringUtils
|
||
|
||
|
||
class CleanInvalidSeed(_PluginBase):
|
||
# 插件名称
|
||
plugin_name = "清理QB无效做种"
|
||
# 插件描述
|
||
plugin_desc = "清理已经被站点删除的种子及源文件,仅支持QB"
|
||
# 插件图标
|
||
plugin_icon = "clean_a.png"
|
||
# 插件版本
|
||
plugin_version = "2.0"
|
||
# 插件作者
|
||
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
|
||
_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
|
||
_downloaders = []
|
||
# 定时器
|
||
_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._downloaders = config.get("downloaders")
|
||
|
||
# 加载模块
|
||
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,
|
||
"downloaders": self._downloaders,
|
||
}
|
||
)
|
||
|
||
@property
|
||
def service_info(self) -> Optional[Dict[str, ServiceInfo]]:
|
||
"""
|
||
服务信息
|
||
"""
|
||
if not self._downloaders:
|
||
logger.warning("尚未配置下载器,请检查配置")
|
||
return None
|
||
|
||
services = DownloaderHelper().get_services(name_filters=self._downloaders)
|
||
|
||
if not services:
|
||
logger.warning("获取下载器实例失败,请检查配置")
|
||
return None
|
||
|
||
active_services = {}
|
||
for service_name, service_info in services.items():
|
||
if service_info.instance.is_inactive():
|
||
logger.warning(f"下载器 {service_name} 未连接,请检查配置")
|
||
elif not self.check_is_qb(service_info):
|
||
logger.warning(f"不支持的下载器类型 {service_name},仅支持QB,请检查配置")
|
||
else:
|
||
active_services[service_name] = service_info
|
||
|
||
if not active_services:
|
||
logger.warning("没有已连接的下载器,请检查配置")
|
||
return None
|
||
|
||
return active_services
|
||
|
||
@staticmethod
|
||
def check_is_qb(service_info) -> bool:
|
||
"""
|
||
检查下载器类型是否为 qbittorrent 或 transmission
|
||
"""
|
||
if DownloaderHelper().is_downloader(service_type="qbittorrent", service=service_info):
|
||
return True
|
||
|
||
return False
|
||
|
||
@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, service):
|
||
downloader_name = service.name
|
||
downloader_obj = service.instance
|
||
all_torrents, error = downloader_obj.get_torrents()
|
||
|
||
if error:
|
||
logger.error(f"获取下载器:{downloader_name}种子失败: {error}")
|
||
if self._notify:
|
||
self.post_message(
|
||
mtype=NotificationType.SiteMessage,
|
||
title=f"【清理QB无效做种】",
|
||
text=f"获取下载器:{downloader_name}种子失败,请检查下载器配置",
|
||
)
|
||
return []
|
||
|
||
if not all_torrents:
|
||
logger.warning(f"下载器:{downloader_name}")
|
||
if self._notify:
|
||
self.post_message(
|
||
mtype=NotificationType.SiteMessage,
|
||
title=f"【清理QB无效做种】",
|
||
text=f"下载器:{downloader_name}中没有种子",
|
||
)
|
||
return []
|
||
return all_torrents
|
||
|
||
def clean_invalid_seed(self):
|
||
for service in self.service_info.values():
|
||
downloader_name = service.name
|
||
downloader_obj = service.instance
|
||
if not downloader_obj:
|
||
logger.error(f"获取下载器失败 {downloader_name}")
|
||
continue
|
||
logger.info(f"开始清理 {downloader_name} 无效做种...")
|
||
all_torrents = self.get_all_torrents(service)
|
||
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:
|
||
# 仅标记
|
||
downloader_obj.set_torrents_tag(ids=torrent.get("hash"), tags=[
|
||
self._label if self._label != "" else "无效做种"])
|
||
else:
|
||
# 只删除种子不删除文件,以防其它站点辅种
|
||
downloader_obj.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"
|
||
|
||
exclude_categories_msg = ""
|
||
exclude_labels_msg = ""
|
||
deleted_msg = ""
|
||
|
||
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 = []
|
||
|
||
for service in self.service_info.values():
|
||
downloader_name = service.name
|
||
downloader_obj = service.instance
|
||
if not downloader_obj:
|
||
logger.error(f"获取下载器失败 {downloader_name}")
|
||
continue
|
||
all_torrents += self.get_all_torrents(service)
|
||
|
||
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("检测无效源文件任务结束")
|
||
|
||
@staticmethod
|
||
def get_size(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
|
||
},
|
||
'content': [
|
||
{
|
||
'component': 'VSelect',
|
||
'props': {
|
||
'multiple': True,
|
||
'chips': True,
|
||
'clearable': True,
|
||
'model': 'downloaders',
|
||
'label': '请选择下载器',
|
||
'items': [{"title": config.name, "value": config.name}
|
||
for config in DownloaderHelper().get_configs().values()]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"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))
|