add plugins
This commit is contained in:
parent
d682c5620a
commit
3e9091f0ef
701
app/plugins/iyuuautoseed/__init__.py
Normal file
701
app/plugins/iyuuautoseed/__init__.py
Normal file
@ -0,0 +1,701 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from lxml import etree
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.plugins.iyuuautoseed.iyuu_helper import IyuuHelper
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class IYUUAutoSeed(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "IYUU自动辅种"
|
||||
# 插件描述
|
||||
plugin_desc = "基于IYUU官方Api实现自动辅种。"
|
||||
# 插件图标
|
||||
plugin_icon = "iyuu.png"
|
||||
# 主题色
|
||||
plugin_color = "#F3B70B"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "iyuuautoseed_"
|
||||
# 加载顺序
|
||||
plugin_order = 17
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
_scheduler = None
|
||||
iyuuhelper = None
|
||||
sites = None
|
||||
# 开关
|
||||
_enabled = False
|
||||
_cron = None
|
||||
_onlyonce = False
|
||||
_token = None
|
||||
_downloaders = []
|
||||
_sites = []
|
||||
_notify = False
|
||||
_nolabels = None
|
||||
_clearcache = False
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
# 种子链接xpaths
|
||||
_torrent_xpaths = [
|
||||
"//form[contains(@action, 'download.php?id=')]/@action",
|
||||
"//a[contains(@href, 'download.php?hash=')]/@href",
|
||||
"//a[contains(@href, 'download.php?id=')]/@href",
|
||||
"//a[@class='index'][contains(@href, '/dl/')]/@href",
|
||||
]
|
||||
_torrent_tags = ["已整理", "辅种"]
|
||||
# 待校全种子hash清单
|
||||
_recheck_torrents = {}
|
||||
_is_recheck_running = False
|
||||
# 辅种缓存,出错的种子不再重复辅种,可清除
|
||||
_error_caches = []
|
||||
# 辅种缓存,辅种成功的种子,可清除
|
||||
_success_caches = []
|
||||
# 辅种缓存,出错的种子不再重复辅种,且无法清除。种子被删除404等情况
|
||||
_permanent_error_caches = []
|
||||
# 辅种计数
|
||||
total = 0
|
||||
realtotal = 0
|
||||
success = 0
|
||||
exist = 0
|
||||
fail = 0
|
||||
cached = 0
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 读取配置
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._cron = config.get("cron")
|
||||
self._token = config.get("token")
|
||||
self._downloaders = config.get("downloaders")
|
||||
self._sites = config.get("sites")
|
||||
self._notify = config.get("notify")
|
||||
self._nolabels = config.get("nolabels")
|
||||
self._clearcache = config.get("clearcache")
|
||||
self._permanent_error_caches = config.get("permanent_error_caches") or []
|
||||
self._error_caches = [] if self._clearcache else config.get("error_caches") or []
|
||||
self._success_caches = [] if self._clearcache else config.get("success_caches") or []
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 启动定时任务 & 立即运行一次
|
||||
if self.get_state() or self._onlyonce:
|
||||
self.iyuuhelper = IyuuHelper(token=self._token)
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(self.auto_seed,
|
||||
CronTrigger.from_crontab(self._cron))
|
||||
logger.info(f"辅种服务启动,周期:{self._cron}")
|
||||
except Exception as err:
|
||||
logger.error(f"辅种服务启动失败:{str(err)}")
|
||||
self.systemmessage.put(f"辅种服务启动失败:{str(err)}")
|
||||
if self._onlyonce:
|
||||
logger.info(f"辅种服务启动,立即运行一次")
|
||||
self._scheduler.add_job(self.auto_seed, 'date',
|
||||
run_date=datetime.now(
|
||||
tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3)
|
||||
)
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
|
||||
if self._clearcache:
|
||||
# 关闭清除缓存开关
|
||||
self._clearcache = False
|
||||
|
||||
if self._clearcache or self._onlyonce:
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
if self._scheduler.get_jobs():
|
||||
# 追加种子校验服务
|
||||
self._scheduler.add_job(self.check_recheck, 'interval', minutes=3)
|
||||
# 启动服务
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return True if self._enabled and self._cron and self._token and self._downloaders else 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': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enable": False,
|
||||
"onlyonce": False,
|
||||
"notify": False,
|
||||
"clearcache": False,
|
||||
"cron": "",
|
||||
"token": "",
|
||||
"downloaders": [],
|
||||
"sites": [],
|
||||
"nolabels": ""
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def __update_config(self):
|
||||
self.update_config({
|
||||
"enable": self._enabled,
|
||||
"onlyonce": self._onlyonce,
|
||||
"clearcache": self._clearcache,
|
||||
"cron": self._cron,
|
||||
"token": self._token,
|
||||
"downloaders": self._downloaders,
|
||||
"sites": self._sites,
|
||||
"notify": self._notify,
|
||||
"nolabels": self._nolabels,
|
||||
"success_caches": self._success_caches,
|
||||
"error_caches": self._error_caches,
|
||||
"permanent_error_caches": self._permanent_error_caches
|
||||
})
|
||||
|
||||
def auto_seed(self):
|
||||
"""
|
||||
开始辅种
|
||||
"""
|
||||
if not self._enabled or not self._token or not self._downloaders:
|
||||
logger.warn("辅种服务未启用或未配置")
|
||||
return
|
||||
if not self.iyuuhelper:
|
||||
return
|
||||
logger.info("开始辅种任务 ...")
|
||||
# 计数器初始化
|
||||
self.total = 0
|
||||
self.realtotal = 0
|
||||
self.success = 0
|
||||
self.exist = 0
|
||||
self.fail = 0
|
||||
self.cached = 0
|
||||
# 扫描下载器辅种
|
||||
for downloader in self._downloaders:
|
||||
logger.info(f"开始扫描下载器 {downloader} ...")
|
||||
# TODO 获取下载器中已完成的种子
|
||||
torrents = []
|
||||
if torrents:
|
||||
logger.info(f"下载器 {downloader} 已完成种子数:{len(torrents)}")
|
||||
else:
|
||||
logger.info(f"下载器 {downloader} 没有已完成种子")
|
||||
continue
|
||||
hash_strs = []
|
||||
for torrent in torrents:
|
||||
if self._event.is_set():
|
||||
logger.info(f"辅种服务停止")
|
||||
return
|
||||
# 获取种子hash
|
||||
hash_str = self.__get_hash(torrent, downloader)
|
||||
if hash_str in self._error_caches or hash_str in self._permanent_error_caches:
|
||||
logger.info(f"种子 {hash_str} 辅种失败且已缓存,跳过 ...")
|
||||
continue
|
||||
save_path = self.__get_save_path(torrent, downloader)
|
||||
# 获取种子标签
|
||||
torrent_labels = self.__get_label(torrent, downloader)
|
||||
if torrent_labels and self._nolabels:
|
||||
is_skip = False
|
||||
for label in self._nolabels.split(','):
|
||||
if label in torrent_labels:
|
||||
logger.info(f"种子 {hash_str} 含有不转移标签 {label},跳过 ...")
|
||||
is_skip = True
|
||||
break
|
||||
if is_skip:
|
||||
continue
|
||||
hash_strs.append({
|
||||
"hash": hash_str,
|
||||
"save_path": save_path
|
||||
})
|
||||
if hash_strs:
|
||||
logger.info(f"总共需要辅种的种子数:{len(hash_strs)}")
|
||||
# 分组处理,减少IYUU Api请求次数
|
||||
chunk_size = 200
|
||||
for i in range(0, len(hash_strs), chunk_size):
|
||||
# 切片操作
|
||||
chunk = hash_strs[i:i + chunk_size]
|
||||
# 处理分组
|
||||
self.__seed_torrents(hash_strs=chunk,
|
||||
downloader=downloader)
|
||||
# 触发校验检查
|
||||
self.check_recheck()
|
||||
else:
|
||||
logger.info(f"没有需要辅种的种子")
|
||||
# 保存缓存
|
||||
self.__update_config()
|
||||
# 发送消息
|
||||
if self._notify:
|
||||
if self.success or self.fail:
|
||||
self.post_message(
|
||||
title="【IYUU自动辅种任务完成】",
|
||||
text=f"服务器返回可辅种总数:{self.total}\n"
|
||||
f"实际可辅种数:{self.realtotal}\n"
|
||||
f"已存在:{self.exist}\n"
|
||||
f"成功:{self.success}\n"
|
||||
f"失败:{self.fail}\n"
|
||||
f"{self.cached} 条失败记录已加入缓存"
|
||||
)
|
||||
logger.info("辅种任务执行完成")
|
||||
|
||||
def check_recheck(self):
|
||||
"""
|
||||
定时检查下载器中种子是否校验完成,校验完成且完整的自动开始辅种
|
||||
"""
|
||||
if not self._recheck_torrents:
|
||||
return
|
||||
if self._is_recheck_running:
|
||||
return
|
||||
self._is_recheck_running = True
|
||||
for downloader in self._downloaders:
|
||||
# 需要检查的种子
|
||||
recheck_torrents = self._recheck_torrents.get(downloader) or []
|
||||
if not recheck_torrents:
|
||||
continue
|
||||
logger.info(f"开始检查下载器 {downloader} 的校验任务 ...")
|
||||
# 下载器类型
|
||||
# TODO 获取下载器中的种子
|
||||
torrents = []
|
||||
if torrents:
|
||||
can_seeding_torrents = []
|
||||
for torrent in torrents:
|
||||
# 获取种子hash
|
||||
hash_str = self.__get_hash(torrent, downloader)
|
||||
if self.__can_seeding(torrent, downloader):
|
||||
can_seeding_torrents.append(hash_str)
|
||||
if can_seeding_torrents:
|
||||
logger.info(f"共 {len(can_seeding_torrents)} 个任务校验完成,开始辅种 ...")
|
||||
# TODO 开始任务
|
||||
# 去除已经处理过的种子
|
||||
self._recheck_torrents[downloader] = list(
|
||||
set(recheck_torrents).difference(set(can_seeding_torrents)))
|
||||
elif torrents is None:
|
||||
logger.info(f"下载器 {downloader} 查询校验任务失败,将在下次继续查询 ...")
|
||||
continue
|
||||
else:
|
||||
logger.info(f"下载器 {downloader} 中没有需要检查的校验任务,清空待处理列表 ...")
|
||||
self._recheck_torrents[downloader] = []
|
||||
self._is_recheck_running = False
|
||||
|
||||
def __seed_torrents(self, hash_strs: list, downloader: str):
|
||||
"""
|
||||
执行一批种子的辅种
|
||||
"""
|
||||
if not hash_strs:
|
||||
return
|
||||
logger.info(f"下载器 {downloader} 开始查询辅种,数量:{len(hash_strs)} ...")
|
||||
# 下载器中的Hashs
|
||||
hashs = [item.get("hash") for item in hash_strs]
|
||||
# 每个Hash的保存目录
|
||||
save_paths = {}
|
||||
for item in hash_strs:
|
||||
save_paths[item.get("hash")] = item.get("save_path")
|
||||
# 查询可辅种数据
|
||||
seed_list, msg = self.iyuuhelper.get_seed_info(hashs)
|
||||
if not isinstance(seed_list, dict):
|
||||
logger.warn(f"当前种子列表没有可辅种的站点:{msg}")
|
||||
return
|
||||
else:
|
||||
logger.info(f"IYUU返回可辅种数:{len(seed_list)}")
|
||||
# 遍历
|
||||
for current_hash, seed_info in seed_list.items():
|
||||
if not seed_info:
|
||||
continue
|
||||
seed_torrents = seed_info.get("torrent")
|
||||
if not isinstance(seed_torrents, list):
|
||||
seed_torrents = [seed_torrents]
|
||||
|
||||
# 本次辅种成功的种子
|
||||
success_torrents = []
|
||||
|
||||
for seed in seed_torrents:
|
||||
if not seed:
|
||||
continue
|
||||
if not isinstance(seed, dict):
|
||||
continue
|
||||
if not seed.get("sid") or not seed.get("info_hash"):
|
||||
continue
|
||||
if seed.get("info_hash") in hashs:
|
||||
logger.info(f"{seed.get('info_hash')} 已在下载器中,跳过 ...")
|
||||
continue
|
||||
if seed.get("info_hash") in self._success_caches:
|
||||
logger.info(f"{seed.get('info_hash')} 已处理过辅种,跳过 ...")
|
||||
continue
|
||||
if seed.get("info_hash") in self._error_caches or seed.get("info_hash") in self._permanent_error_caches:
|
||||
logger.info(f"种子 {seed.get('info_hash')} 辅种失败且已缓存,跳过 ...")
|
||||
continue
|
||||
# 添加任务
|
||||
success = self.__download_torrent(seed=seed,
|
||||
downloader=downloader,
|
||||
save_path=save_paths.get(current_hash))
|
||||
if success:
|
||||
success_torrents.append(seed.get("info_hash"))
|
||||
|
||||
# 辅种成功的去重放入历史
|
||||
if len(success_torrents) > 0:
|
||||
self.__save_history(current_hash=current_hash,
|
||||
downloader=downloader,
|
||||
success_torrents=success_torrents)
|
||||
|
||||
logger.info(f"下载器 {downloader} 辅种完成")
|
||||
|
||||
def __save_history(self, current_hash: str, downloader: str, success_torrents: []):
|
||||
"""
|
||||
[
|
||||
{
|
||||
"downloader":"2",
|
||||
"torrents":[
|
||||
"248103a801762a66c201f39df7ea325f8eda521b",
|
||||
"bd13835c16a5865b01490962a90b3ec48889c1f0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"downloader":"3",
|
||||
"torrents":[
|
||||
"248103a801762a66c201f39df7ea325f8eda521b",
|
||||
"bd13835c16a5865b01490962a90b3ec48889c1f0"
|
||||
]
|
||||
}
|
||||
]
|
||||
"""
|
||||
try:
|
||||
# 查询当前Hash的辅种历史
|
||||
seed_history = self.get_data(key=current_hash) or []
|
||||
|
||||
new_history = True
|
||||
if len(seed_history) > 0:
|
||||
for history in seed_history:
|
||||
if not history:
|
||||
continue
|
||||
if not isinstance(history, dict):
|
||||
continue
|
||||
if not history.get("downloader"):
|
||||
continue
|
||||
# 如果本次辅种下载器之前有过记录则继续添加
|
||||
if int(history.get("downloader")) == downloader:
|
||||
history_torrents = history.get("torrents") or []
|
||||
history["torrents"] = list(set(history_torrents + success_torrents))
|
||||
new_history = False
|
||||
break
|
||||
|
||||
# 本次辅种下载器之前没有成功记录则新增
|
||||
if new_history:
|
||||
seed_history.append({
|
||||
"downloader": downloader,
|
||||
"torrents": list(set(success_torrents))
|
||||
})
|
||||
|
||||
# 保存历史
|
||||
self.save_data(key=current_hash,
|
||||
value=seed_history)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
def __download_torrent(self, seed: dict, downloader: str, save_path: str):
|
||||
"""
|
||||
下载种子
|
||||
torrent: {
|
||||
"sid": 3,
|
||||
"torrent_id": 377467,
|
||||
"info_hash": "a444850638e7a6f6220e2efdde94099c53358159"
|
||||
}
|
||||
"""
|
||||
self.total += 1
|
||||
# 获取种子站点及下载地址模板
|
||||
site_url, download_page = self.iyuuhelper.get_torrent_url(seed.get("sid"))
|
||||
if not site_url or not download_page:
|
||||
# 加入缓存
|
||||
self._error_caches.append(seed.get("info_hash"))
|
||||
self.fail += 1
|
||||
self.cached += 1
|
||||
return False
|
||||
# 查询站点
|
||||
site_info = self.sites.get_sites(siteurl=site_url)
|
||||
if not site_info:
|
||||
logger.debug(f"没有维护种子对应的站点:{site_url}")
|
||||
return False
|
||||
if self._sites and str(site_info.get("id")) not in self._sites:
|
||||
logger.info("当前站点不在选择的辅助站点范围,跳过 ...")
|
||||
return False
|
||||
self.realtotal += 1
|
||||
# TODO 查询hash值是否已经在下载器中
|
||||
torrent_info = []
|
||||
if torrent_info:
|
||||
logger.debug(f"{seed.get('info_hash')} 已在下载器中,跳过 ...")
|
||||
self.exist += 1
|
||||
return False
|
||||
# 站点流控
|
||||
if self.sites.check_ratelimit(site_info.get("id")):
|
||||
self.fail += 1
|
||||
return False
|
||||
# 下载种子
|
||||
torrent_url = self.__get_download_url(seed=seed,
|
||||
site=site_info,
|
||||
base_url=download_page)
|
||||
if not torrent_url:
|
||||
# 加入失败缓存
|
||||
self._error_caches.append(seed.get("info_hash"))
|
||||
self.fail += 1
|
||||
self.cached += 1
|
||||
return False
|
||||
# 强制使用Https
|
||||
if "?" in torrent_url:
|
||||
torrent_url += "&https=1"
|
||||
else:
|
||||
torrent_url += "?https=1"
|
||||
# TODO 添加下载,辅种任务默认暂停
|
||||
download_id, retmsg = None, ""
|
||||
if not download_id:
|
||||
# 下载失败
|
||||
logger.warn(f"添加下载任务出错,"
|
||||
f"错误原因:{retmsg or '下载器添加任务失败'},"
|
||||
f"种子链接:{torrent_url}")
|
||||
self.fail += 1
|
||||
# 加入失败缓存
|
||||
if retmsg and ('无法打开链接' in retmsg or '触发站点流控' in retmsg):
|
||||
self._error_caches.append(seed.get("info_hash"))
|
||||
else:
|
||||
# 种子不存在的情况
|
||||
self._permanent_error_caches.append(seed.get("info_hash"))
|
||||
return False
|
||||
else:
|
||||
self.success += 1
|
||||
# 追加校验任务
|
||||
logger.info(f"添加校验检查任务:{download_id} ...")
|
||||
if not self._recheck_torrents.get(downloader):
|
||||
self._recheck_torrents[downloader] = []
|
||||
self._recheck_torrents[downloader].append(download_id)
|
||||
# 下载成功
|
||||
logger.info(f"成功添加辅种下载,站点:{site_info.get('name')},种子链接:{torrent_url}")
|
||||
# TR会自动校验
|
||||
if downloader == "qbittorrent":
|
||||
# TODO 开始校验种子
|
||||
pass
|
||||
|
||||
# 成功也加入缓存,有一些改了路径校验不通过的,手动删除后,下一次又会辅上
|
||||
self._success_caches.append(seed.get("info_hash"))
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __get_hash(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子hash
|
||||
"""
|
||||
try:
|
||||
return torrent.get("hash") if dl_type == "qbittorrent" else torrent.hashString
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def __get_label(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子标签
|
||||
"""
|
||||
try:
|
||||
return torrent.get("tags") or [] if dl_type == "qbittorrent" else torrent.labels or []
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def __can_seeding(torrent: Any, dl_type: str):
|
||||
"""
|
||||
判断种子是否可以做种并处于暂停状态
|
||||
"""
|
||||
try:
|
||||
return torrent.get("state") == "pausedUP" if dl_type == "qbittorrent" \
|
||||
else (torrent.status.stopped and torrent.percent_done == 1)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __get_save_path(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子保存路径
|
||||
"""
|
||||
try:
|
||||
return torrent.get("save_path") if dl_type == "qbittorrent" else torrent.download_dir
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return ""
|
||||
|
||||
def __get_download_url(self, seed: dict, site: dict, base_url: str):
|
||||
"""
|
||||
拼装种子下载链接
|
||||
"""
|
||||
|
||||
def __is_special_site(url):
|
||||
"""
|
||||
判断是否为特殊站点
|
||||
"""
|
||||
spec_params = ["hash=", "authkey="]
|
||||
if any(field in base_url for field in spec_params):
|
||||
return True
|
||||
if "hdchina.org" in url:
|
||||
return True
|
||||
if "hdsky.me" in url:
|
||||
return True
|
||||
if "hdcity.in" in url:
|
||||
return True
|
||||
if "totheglory.im" in url:
|
||||
return True
|
||||
return False
|
||||
|
||||
try:
|
||||
if __is_special_site(site.get('strict_url')):
|
||||
# 从详情页面获取下载链接
|
||||
return self.__get_torrent_url_from_page(seed=seed, site=site)
|
||||
else:
|
||||
download_url = base_url.replace(
|
||||
"id={}",
|
||||
"id={id}"
|
||||
).replace(
|
||||
"/{}",
|
||||
"/{id}"
|
||||
).replace(
|
||||
"/{torrent_key}",
|
||||
""
|
||||
).format(
|
||||
**{
|
||||
"id": seed.get("torrent_id"),
|
||||
"passkey": site.get("passkey") or '',
|
||||
"uid": site.get("uid") or '',
|
||||
}
|
||||
)
|
||||
if download_url.count("{"):
|
||||
logger.warn(f"当前不支持该站点的辅助任务,Url转换失败:{seed}")
|
||||
return None
|
||||
download_url = re.sub(r"[&?]passkey=", "",
|
||||
re.sub(r"[&?]uid=", "",
|
||||
download_url,
|
||||
flags=re.IGNORECASE),
|
||||
flags=re.IGNORECASE)
|
||||
return f"{site.get('strict_url')}/{download_url}"
|
||||
except Exception as e:
|
||||
logger.warn(f"站点 {site.get('name')} Url转换失败:{str(e)},尝试通过详情页面获取种子下载链接 ...")
|
||||
return self.__get_torrent_url_from_page(seed=seed, site=site)
|
||||
|
||||
def __get_torrent_url_from_page(self, seed: dict, site: dict):
|
||||
"""
|
||||
从详情页面获取下载链接
|
||||
"""
|
||||
try:
|
||||
page_url = f"{site.get('strict_url')}/details.php?id={seed.get('torrent_id')}&hit=1"
|
||||
logger.info(f"正在获取种子下载链接:{page_url} ...")
|
||||
res = RequestUtils(
|
||||
cookies=site.get("cookie"),
|
||||
headers=site.get("ua"),
|
||||
proxies=settings.PROXY if site.get("proxy") else None
|
||||
).get_res(url=page_url)
|
||||
if res is not None and res.status_code in (200, 500):
|
||||
if "charset=utf-8" in res.text or "charset=UTF-8" in res.text:
|
||||
res.encoding = "UTF-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
if not res.text:
|
||||
logger.warn(f"获取种子下载链接失败,页面内容为空:{page_url}")
|
||||
return None
|
||||
# 使用xpath从页面中获取下载链接
|
||||
html = etree.HTML(res.text)
|
||||
for xpath in self._torrent_xpaths:
|
||||
download_url = html.xpath(xpath)
|
||||
if download_url:
|
||||
download_url = download_url[0]
|
||||
logger.info(f"获取种子下载链接成功:{download_url}")
|
||||
if not download_url.startswith("http"):
|
||||
if download_url.startswith("/"):
|
||||
download_url = f"{site.get('strict_url')}{download_url}"
|
||||
else:
|
||||
download_url = f"{site.get('strict_url')}/{download_url}"
|
||||
return download_url
|
||||
logger.warn(f"获取种子下载链接失败,未找到下载链接:{page_url}")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"获取种子下载链接失败,请求失败:{page_url},{res.status_code if res else ''}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn(f"获取种子下载链接失败:{str(e)}")
|
||||
return None
|
||||
|
||||
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))
|
166
app/plugins/iyuuautoseed/iyuu_helper.py
Normal file
166
app/plugins/iyuuautoseed/iyuu_helper.py
Normal file
@ -0,0 +1,166 @@
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class IyuuHelper(object):
|
||||
_version = "2.0.0"
|
||||
_api_base = "https://api.iyuu.cn/%s"
|
||||
_sites = {}
|
||||
_token = None
|
||||
|
||||
def __init__(self, token):
|
||||
self._token = token
|
||||
if self._token:
|
||||
self.init_config()
|
||||
|
||||
def init_config(self):
|
||||
pass
|
||||
|
||||
def __request_iyuu(self, url, method="get", params=None):
|
||||
"""
|
||||
向IYUUApi发送请求
|
||||
"""
|
||||
if params:
|
||||
if not params.get("sign"):
|
||||
params.update({"sign": self._token})
|
||||
if not params.get("version"):
|
||||
params.update({"version": self._version})
|
||||
else:
|
||||
params = {"sign": self._token, "version": self._version}
|
||||
# 开始请求
|
||||
if method == "get":
|
||||
ret = RequestUtils(
|
||||
accept_type="application/json"
|
||||
).get_res(f"{url}", params=params)
|
||||
else:
|
||||
ret = RequestUtils(
|
||||
accept_type="application/json"
|
||||
).post_res(f"{url}", data=params)
|
||||
if ret:
|
||||
result = ret.json()
|
||||
if result.get('ret') == 200:
|
||||
return result.get('data'), ""
|
||||
else:
|
||||
return None, f"请求IYUU失败,状态码:{result.get('ret')},返回信息:{result.get('msg')}"
|
||||
elif ret is not None:
|
||||
return None, f"请求IYUU失败,状态码:{ret.status_code},错误原因:{ret.reason}"
|
||||
else:
|
||||
return None, f"请求IYUU失败,未获取到返回信息"
|
||||
|
||||
def get_torrent_url(self, sid):
|
||||
if not sid:
|
||||
return None, None
|
||||
if not self._sites:
|
||||
self._sites = self.__get_sites()
|
||||
if not self._sites.get(sid):
|
||||
return None, None
|
||||
site = self._sites.get(sid)
|
||||
return site.get('base_url'), site.get('download_page')
|
||||
|
||||
def __get_sites(self):
|
||||
"""
|
||||
返回支持辅种的全部站点
|
||||
:return: 站点列表、错误信息
|
||||
{
|
||||
"ret": 200,
|
||||
"data": {
|
||||
"sites": [
|
||||
{
|
||||
"id": 1,
|
||||
"site": "keepfrds",
|
||||
"nickname": "朋友",
|
||||
"base_url": "pt.keepfrds.com",
|
||||
"download_page": "download.php?id={}&passkey={passkey}",
|
||||
"reseed_check": "passkey",
|
||||
"is_https": 2
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
result, msg = self.__request_iyuu(url=self._api_base % 'App.Api.Sites')
|
||||
if result:
|
||||
ret_sites = {}
|
||||
sites = result.get('sites') or []
|
||||
for site in sites:
|
||||
ret_sites[site.get('id')] = site
|
||||
return ret_sites
|
||||
else:
|
||||
print(msg)
|
||||
return {}
|
||||
|
||||
def get_seed_info(self, info_hashs: list):
|
||||
"""
|
||||
返回info_hash对应的站点id、种子id
|
||||
{
|
||||
"ret": 200,
|
||||
"data": [
|
||||
{
|
||||
"sid": 3,
|
||||
"torrent_id": 377467,
|
||||
"info_hash": "a444850638e7a6f6220e2efdde94099c53358159"
|
||||
},
|
||||
{
|
||||
"sid": 7,
|
||||
"torrent_id": 35538,
|
||||
"info_hash": "cf7d88fd656d10fe5130d13567aec27068b96676"
|
||||
}
|
||||
],
|
||||
"msg": "",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
"""
|
||||
info_hashs.sort()
|
||||
json_data = json.dumps(info_hashs, separators=(',', ':'), ensure_ascii=False)
|
||||
sha1 = self.get_sha1(json_data)
|
||||
result, msg = self.__request_iyuu(url=self._api_base % 'App.Api.Infohash',
|
||||
method="post",
|
||||
params={
|
||||
"timestamp": time.time(),
|
||||
"hash": json_data,
|
||||
"sha1": sha1
|
||||
})
|
||||
return result, msg
|
||||
|
||||
@staticmethod
|
||||
def get_sha1(json_str) -> str:
|
||||
return hashlib.sha1(json_str.encode('utf-8')).hexdigest()
|
||||
|
||||
def get_auth_sites(self):
|
||||
"""
|
||||
返回支持鉴权的站点列表
|
||||
[
|
||||
{
|
||||
"id": 2,
|
||||
"site": "pthome",
|
||||
"bind_check": "passkey,uid"
|
||||
}
|
||||
]
|
||||
"""
|
||||
result, msg = self.__request_iyuu(url=self._api_base % 'App.Api.GetRecommendSites')
|
||||
if result:
|
||||
return result.get('recommend') or []
|
||||
else:
|
||||
print(msg)
|
||||
return []
|
||||
|
||||
def bind_site(self, site, passkey, uid):
|
||||
"""
|
||||
绑定站点
|
||||
:param site: 站点名称
|
||||
:param passkey: passkey
|
||||
:param uid: 用户id
|
||||
:return: 状态码、错误信息
|
||||
"""
|
||||
result, msg = self.__request_iyuu(url=self._api_base % 'App.Api.Bind',
|
||||
method="get",
|
||||
params={
|
||||
"token": self._token,
|
||||
"site": site,
|
||||
"passkey": self.get_sha1(passkey),
|
||||
"id": uid
|
||||
})
|
||||
return result, msg
|
514
app/plugins/torrenttransfer/__init__.py
Normal file
514
app/plugins/torrenttransfer/__init__.py
Normal file
@ -0,0 +1,514 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
import pytz
|
||||
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
|
||||
|
||||
|
||||
class TorrentTransfer(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "自动转移做种"
|
||||
# 插件描述
|
||||
plugin_desc = "定期转移下载器中的做种任务到另一个下载器。"
|
||||
# 插件图标
|
||||
plugin_icon = "torrenttransfer.jpg"
|
||||
# 主题色
|
||||
plugin_color = "#272636"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "torrenttransfer_"
|
||||
# 加载顺序
|
||||
plugin_order = 18
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
_scheduler = None
|
||||
sites = None
|
||||
# 开关
|
||||
_enabled = False
|
||||
_cron = None
|
||||
_onlyonce = False
|
||||
_fromdownloader = None
|
||||
_todownloader = None
|
||||
_frompath = None
|
||||
_topath = None
|
||||
_notify = False
|
||||
_nolabels = None
|
||||
_nopaths = None
|
||||
_deletesource = False
|
||||
_fromtorrentpath = None
|
||||
_autostart = False
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
# 待检查种子清单
|
||||
_recheck_torrents = {}
|
||||
_is_recheck_running = False
|
||||
# 任务标签
|
||||
_torrent_tags = ["已整理", "转移做种"]
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 读取配置
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._cron = config.get("cron")
|
||||
self._notify = config.get("notify")
|
||||
self._nolabels = config.get("nolabels")
|
||||
self._frompath = config.get("frompath")
|
||||
self._topath = config.get("topath")
|
||||
self._fromdownloader = config.get("fromdownloader")
|
||||
self._todownloader = config.get("todownloader")
|
||||
self._deletesource = config.get("deletesource")
|
||||
self._fromtorrentpath = config.get("fromtorrentpath")
|
||||
self._nopaths = config.get("nopaths")
|
||||
self._autostart = config.get("autostart")
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 启动定时任务 & 立即运行一次
|
||||
if self.get_state() or self._onlyonce:
|
||||
# 检查配置
|
||||
if self._fromtorrentpath and not Path(self._fromtorrentpath).exists():
|
||||
logger.error(f"源下载器种子文件保存路径不存在:{self._fromtorrentpath}")
|
||||
return
|
||||
if self._fromdownloader == self._todownloader:
|
||||
logger.error(f"源下载器和目的下载器不能相同")
|
||||
self.systemmessage(f"源下载器和目的下载器不能相同")
|
||||
return
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
logger.info(f"移转做种服务启动,周期:{self._cron}")
|
||||
try:
|
||||
self._scheduler.add_job(self.transfer,
|
||||
CronTrigger.from_crontab(self._cron))
|
||||
except Exception as e:
|
||||
logger.error(f"移转做种服务启动失败:{e}")
|
||||
self.systemmessage(f"移转做种服务启动失败:{e}")
|
||||
return
|
||||
if self._onlyonce:
|
||||
logger.info(f"移转做种服务启动,立即运行一次")
|
||||
self._scheduler.add_job(self.transfer, 'date',
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(
|
||||
seconds=3))
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
self.update_config({
|
||||
"enable": self._enabled,
|
||||
"onlyonce": self._onlyonce,
|
||||
"cron": self._cron,
|
||||
"notify": self._notify,
|
||||
"nolabels": self._nolabels,
|
||||
"frompath": self._frompath,
|
||||
"topath": self._topath,
|
||||
"fromdownloader": self._fromdownloader,
|
||||
"todownloader": self._todownloader,
|
||||
"deletesource": self._deletesource,
|
||||
"fromtorrentpath": self._fromtorrentpath,
|
||||
"nopaths": self._nopaths,
|
||||
"autostart": self._autostart
|
||||
})
|
||||
if self._scheduler.get_jobs():
|
||||
if self._autostart:
|
||||
# 追加种子校验服务
|
||||
self._scheduler.add_job(self.check_recheck, 'interval', minutes=3)
|
||||
# 启动服务
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def get_state(self):
|
||||
return True if self._enabled \
|
||||
and self._cron \
|
||||
and self._fromdownloader \
|
||||
and self._todownloader \
|
||||
and self._fromtorrentpath else 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': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enable": False,
|
||||
"notify": False,
|
||||
"onlyonce": False,
|
||||
"cron": "",
|
||||
"nolabels": "",
|
||||
"frompath": "",
|
||||
"topath": "",
|
||||
"fromdownloader": "",
|
||||
"todownloader": "",
|
||||
"deletesource": False,
|
||||
"fromtorrentpath": "",
|
||||
"nopaths": "",
|
||||
"autostart": True
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def transfer(self):
|
||||
"""
|
||||
开始移转做种
|
||||
"""
|
||||
if not self._enabled \
|
||||
or not self._fromdownloader \
|
||||
or not self._todownloader \
|
||||
or not self._fromtorrentpath:
|
||||
logger.warn("移转做种服务未启用或未配置")
|
||||
return
|
||||
logger.info("开始移转做种任务 ...")
|
||||
# 源下载器
|
||||
downloader = self._fromdownloader
|
||||
# 目的下载器
|
||||
todownloader = self._todownloader
|
||||
# TODO 获取下载器中已完成的种子
|
||||
torrents = []
|
||||
if torrents:
|
||||
logger.info(f"下载器 {downloader} 已完成种子数:{len(torrents)}")
|
||||
else:
|
||||
logger.info(f"下载器 {downloader} 没有已完成种子")
|
||||
return
|
||||
# 过滤种子,记录保存目录
|
||||
hash_strs = []
|
||||
for torrent in torrents:
|
||||
if self._event.is_set():
|
||||
logger.info(f"移转服务停止")
|
||||
return
|
||||
# 获取种子hash
|
||||
hash_str = self.__get_hash(torrent, downloader)
|
||||
# 获取保存路径
|
||||
save_path = self.__get_save_path(torrent, downloader)
|
||||
if self._nopaths and save_path:
|
||||
# 过滤不需要移转的路径
|
||||
nopath_skip = False
|
||||
for nopath in self._nopaths.split('\n'):
|
||||
if os.path.normpath(save_path).startswith(os.path.normpath(nopath)):
|
||||
logger.info(f"种子 {hash_str} 保存路径 {save_path} 不需要移转,跳过 ...")
|
||||
nopath_skip = True
|
||||
break
|
||||
if nopath_skip:
|
||||
continue
|
||||
# 获取种子标签
|
||||
torrent_labels = self.__get_label(torrent, downloader)
|
||||
if torrent_labels and self._nolabels:
|
||||
is_skip = False
|
||||
for label in self._nolabels.split(','):
|
||||
if label in torrent_labels:
|
||||
logger.info(f"种子 {hash_str} 含有不转移标签 {label},跳过 ...")
|
||||
is_skip = True
|
||||
break
|
||||
if is_skip:
|
||||
continue
|
||||
hash_strs.append({
|
||||
"hash": hash_str,
|
||||
"save_path": save_path
|
||||
})
|
||||
# 开始转移任务
|
||||
if hash_strs:
|
||||
logger.info(f"需要移转的种子数:{len(hash_strs)}")
|
||||
# 记数
|
||||
total = len(hash_strs)
|
||||
success = 0
|
||||
fail = 0
|
||||
for hash_item in hash_strs:
|
||||
# 检查种子文件是否存在
|
||||
torrent_file = os.path.join(self._fromtorrentpath,
|
||||
f"{hash_item.get('hash')}.torrent")
|
||||
if not os.path.exists(torrent_file):
|
||||
logger.error(f"种子文件不存在:{torrent_file}")
|
||||
fail += 1
|
||||
continue
|
||||
# TODO 查询hash值是否已经在目的下载器中
|
||||
torrent_info = []
|
||||
if torrent_info:
|
||||
logger.debug(f"{hash_item.get('hash')} 已在目的下载器中,跳过 ...")
|
||||
continue
|
||||
# 转换保存路径
|
||||
download_dir = self.__convert_save_path(hash_item.get('save_path'),
|
||||
self._frompath,
|
||||
self._topath)
|
||||
if not download_dir:
|
||||
logger.error(f"转换保存路径失败:{hash_item.get('save_path')}")
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
# 如果是QB检查是否有Tracker,没有的话补充解析
|
||||
if downloader == "qbittorrent":
|
||||
# TODO 读取种子内容、解析种子文件
|
||||
content, retmsg = None, ""
|
||||
if not content:
|
||||
logger.error(f"读取种子文件失败:{retmsg}")
|
||||
fail += 1
|
||||
continue
|
||||
# TODO 读取trackers
|
||||
try:
|
||||
torrent_main = None
|
||||
main_announce = None
|
||||
except Exception as err:
|
||||
logger.error(f"解析种子文件 {torrent_file} 失败:{err}")
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
if not main_announce:
|
||||
logger.info(f"{hash_item.get('hash')} 未发现tracker信息,尝试补充tracker信息...")
|
||||
# 读取fastresume文件
|
||||
fastresume_file = os.path.join(self._fromtorrentpath,
|
||||
f"{hash_item.get('hash')}.fastresume")
|
||||
if not os.path.exists(fastresume_file):
|
||||
logger.error(f"fastresume文件不存在:{fastresume_file}")
|
||||
fail += 1
|
||||
continue
|
||||
# 尝试补充trackers
|
||||
try:
|
||||
with open(fastresume_file, 'rb') as f:
|
||||
fastresume = f.read()
|
||||
# TODO 解析fastresume文件
|
||||
torrent_fastresume = None
|
||||
# TODO 读取trackers
|
||||
fastresume_trackers = None
|
||||
if isinstance(fastresume_trackers, list) \
|
||||
and len(fastresume_trackers) > 0 \
|
||||
and fastresume_trackers[0]:
|
||||
# 重新赋值
|
||||
torrent_main['announce'] = fastresume_trackers[0][0]
|
||||
# 替换种子文件路径
|
||||
torrent_file = settings.TEMP_PATH / f"{hash_item.get('hash')}.torrent"
|
||||
# TODO 编码并保存到临时文件
|
||||
with open(torrent_file, 'wb') as f:
|
||||
pass
|
||||
except Exception as err:
|
||||
logger.error(f"解析fastresume文件 {fastresume_file} 失败:{err}")
|
||||
fail += 1
|
||||
continue
|
||||
|
||||
# TODO 发送到另一个下载器中下载:默认暂停、传输下载路径、关闭自动管理模式
|
||||
download_id, retmsg = None, ""
|
||||
if not download_id:
|
||||
# 下载失败
|
||||
logger.warn(f"添加转移任务出错,"
|
||||
f"错误原因:{retmsg or '下载器添加任务失败'},"
|
||||
f"种子文件:{torrent_file}")
|
||||
fail += 1
|
||||
continue
|
||||
else:
|
||||
# 追加校验任务
|
||||
logger.info(f"添加校验检查任务:{download_id} ...")
|
||||
if not self._recheck_torrents.get(todownloader):
|
||||
self._recheck_torrents[todownloader] = []
|
||||
self._recheck_torrents[todownloader].append(download_id)
|
||||
# 下载成功
|
||||
logger.info(f"成功添加转移做种任务,种子文件:{torrent_file}")
|
||||
# TR会自动校验
|
||||
if downloader == "qbittorrent":
|
||||
# TODO 开始校验种子
|
||||
pass
|
||||
# TODO 删除源种子,不能删除文件!
|
||||
if self._deletesource:
|
||||
pass
|
||||
success += 1
|
||||
# 插入转种记录
|
||||
history_key = "%s-%s" % (int(self._fromdownloader[0]), hash_item.get('hash'))
|
||||
self.save_data(key=history_key,
|
||||
value={
|
||||
"to_download": int(self._todownloader[0]),
|
||||
"to_download_id": download_id,
|
||||
"delete_source": self._deletesource,
|
||||
})
|
||||
# 触发校验任务
|
||||
if success > 0 and self._autostart:
|
||||
self.check_recheck()
|
||||
# 发送通知
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
title="【移转做种任务执行完成】",
|
||||
text=f"总数:{total},成功:{success},失败:{fail}"
|
||||
)
|
||||
else:
|
||||
logger.info(f"没有需要移转的种子")
|
||||
logger.info("移转做种任务执行完成")
|
||||
|
||||
def check_recheck(self):
|
||||
"""
|
||||
定时检查下载器中种子是否校验完成,校验完成且完整的自动开始辅种
|
||||
"""
|
||||
if not self._recheck_torrents:
|
||||
return
|
||||
if not self._todownloader:
|
||||
return
|
||||
if self._is_recheck_running:
|
||||
return
|
||||
downloader = self._todownloader
|
||||
# 需要检查的种子
|
||||
recheck_torrents = self._recheck_torrents.get(downloader, [])
|
||||
if not recheck_torrents:
|
||||
return
|
||||
logger.info(f"开始检查下载器 {downloader} 的校验任务 ...")
|
||||
self._is_recheck_running = True
|
||||
# TODO 获取下载器中的种子
|
||||
torrents = []
|
||||
if torrents:
|
||||
can_seeding_torrents = []
|
||||
for torrent in torrents:
|
||||
# 获取种子hash
|
||||
hash_str = self.__get_hash(torrent, downloader)
|
||||
if self.__can_seeding(torrent, downloader):
|
||||
can_seeding_torrents.append(hash_str)
|
||||
if can_seeding_torrents:
|
||||
logger.info(f"共 {len(can_seeding_torrents)} 个任务校验完成,开始辅种 ...")
|
||||
# TODO 开始辅种
|
||||
# 去除已经处理过的种子
|
||||
self._recheck_torrents[downloader] = list(
|
||||
set(recheck_torrents).difference(set(can_seeding_torrents)))
|
||||
elif torrents is None:
|
||||
logger.info(f"下载器 {downloader} 查询校验任务失败,将在下次继续查询 ...")
|
||||
else:
|
||||
logger.info(f"下载器 {downloader} 中没有需要检查的校验任务,清空待处理列表 ...")
|
||||
self._recheck_torrents[downloader] = []
|
||||
self._is_recheck_running = False
|
||||
|
||||
@staticmethod
|
||||
def __get_hash(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子hash
|
||||
"""
|
||||
try:
|
||||
return torrent.get("hash") if dl_type == "qbittorrent" else torrent.hashString
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def __get_label(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子标签
|
||||
"""
|
||||
try:
|
||||
return torrent.get("tags") or [] if dl_type == "qbittorrent" else torrent.labels or []
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def __get_save_path(torrent: Any, dl_type: str):
|
||||
"""
|
||||
获取种子保存路径
|
||||
"""
|
||||
try:
|
||||
return torrent.get("save_path") if dl_type == "qbittorrent" else torrent.download_dir
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def __can_seeding(torrent: Any, dl_type: str):
|
||||
"""
|
||||
判断种子是否可以做种并处于暂停状态
|
||||
"""
|
||||
try:
|
||||
return torrent.get("state") == "pausedUP" and torrent.get("tracker") if dl_type == "qbittorrent" \
|
||||
else (torrent.status.stopped and torrent.percent_done == 1 and torrent.trackers)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __convert_save_path(save_path: str, from_root: str, to_root: str):
|
||||
"""
|
||||
转换保存路径
|
||||
"""
|
||||
try:
|
||||
# 没有保存目录,以目的根目录为准
|
||||
if not save_path:
|
||||
return to_root
|
||||
# 没有设置根目录时返回save_path
|
||||
if not to_root or not from_root:
|
||||
return save_path
|
||||
# 统一目录格式
|
||||
save_path = os.path.normpath(save_path).replace("\\", "/")
|
||||
from_root = os.path.normpath(from_root).replace("\\", "/")
|
||||
to_root = os.path.normpath(to_root).replace("\\", "/")
|
||||
# 替换根目录
|
||||
if save_path.startswith(from_root):
|
||||
return save_path.replace(from_root, to_root, 1)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return None
|
||||
|
||||
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))
|
Loading…
x
Reference in New Issue
Block a user