mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-23 07:26:43 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
24
README.md
24
README.md
@@ -20,7 +20,7 @@ MoviePilot官方插件市场:https://github.com/jxxghp/MoviePilot-Plugins
|
||||
- 可在插件目录中放置`requirement.txt`文件,用于指定插件依赖的第三方库,MoviePilot会在插件安装时自动安装依赖库。
|
||||
|
||||
### 5. 界面开发
|
||||
- 插件支持`插件配置`及`详情展示`两个展示页面,通过配置化的方式组装,使用[Vuetify](https://vuetifyjs.com/)组件库,所有该组件库有的组件都可以通过Json配置使用。
|
||||
- 插件支持`插件配置`及`详情展示`两个展示页面,通过配置化的方式组装,使用 [Vuetify](https://vuetifyjs.com/) 组件库,所有该组件库有的组件都可以通过Json配置使用。
|
||||
|
||||
|
||||
## 常见问题
|
||||
@@ -48,34 +48,38 @@ MoviePilot官方插件市场:https://github.com/jxxghp/MoviePilot-Plugins
|
||||
PS:MoviePilot中的其它事件也是同样方法实现响应:
|
||||
```python
|
||||
class EventType(Enum):
|
||||
# 插件重载
|
||||
# 插件需要重载
|
||||
PluginReload = "plugin.reload"
|
||||
# 插件动作
|
||||
PluginAction = "plugin.action"
|
||||
# 执行命令
|
||||
CommandExcute = "command.excute"
|
||||
# 站点删除
|
||||
# 站点已删除
|
||||
SiteDeleted = "site.deleted"
|
||||
# Webhook消息
|
||||
WebhookMessage = "webhook.message"
|
||||
# 站点已更新
|
||||
SiteUpdated = "site.updated"
|
||||
# 转移完成
|
||||
TransferComplete = "transfer.complete"
|
||||
# 添加下载
|
||||
# 下载已添加
|
||||
DownloadAdded = "download.added"
|
||||
# 删除历史记录
|
||||
HistoryDeleted = "history.deleted"
|
||||
# 删除下载源文件
|
||||
DownloadFileDeleted = "downloadfile.deleted"
|
||||
# 用户外来消息
|
||||
# 收到用户外来消息
|
||||
UserMessage = "user.message"
|
||||
# 通知消息
|
||||
# 收到Webhook消息
|
||||
WebhookMessage = "webhook.message"
|
||||
# 发送消息通知
|
||||
NoticeMessage = "notice.message"
|
||||
# 名称识别请求
|
||||
NameRecognize = "name.recognize"
|
||||
# 名称识别结果
|
||||
NameRecognizeResult = "name.recognize.result"
|
||||
# 站点信息更新
|
||||
SiteUpdated = "site.updated"
|
||||
# 订阅已添加
|
||||
SubscribeAdded = "subscribe.added"
|
||||
# 订阅已完成
|
||||
SubscribeComplete = "subscribe.complete"
|
||||
```
|
||||
|
||||
### 2. 如何在插件中实现远程命令响应?
|
||||
|
||||
BIN
icons/FeiShu_A.png
Normal file
BIN
icons/FeiShu_A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
20
package.json
20
package.json
@@ -185,8 +185,8 @@
|
||||
},
|
||||
"CrossSeed": {
|
||||
"name": "青蛙辅种助手",
|
||||
"description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音等。",
|
||||
"version": "1.8",
|
||||
"description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。",
|
||||
"version": "2.0",
|
||||
"icon": "qingwa.png",
|
||||
"author": "233@qingwa",
|
||||
"level": 2
|
||||
@@ -195,7 +195,7 @@
|
||||
"name": "整理VCB动漫压制组作品",
|
||||
"description": "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理",
|
||||
"version": "1.6.6",
|
||||
"icon": "qingwa.png",
|
||||
"icon": "vcbmonitor.png",
|
||||
"author": "pixel@qingwa",
|
||||
"level": 2
|
||||
},
|
||||
@@ -226,7 +226,7 @@
|
||||
"BrushFlow": {
|
||||
"name": "站点刷流",
|
||||
"description": "自动托管刷流,将会提高对应站点的访问频率。",
|
||||
"version": "2.0",
|
||||
"version": "2.2",
|
||||
"icon": "brush.jpg",
|
||||
"author": "jxxghp",
|
||||
"level": 2
|
||||
@@ -450,7 +450,7 @@
|
||||
"ContractCheck": {
|
||||
"name": "契约检查",
|
||||
"description": "定时检查保种契约达成情况。",
|
||||
"version": "1.0",
|
||||
"version": "1.1",
|
||||
"icon": "contract.png",
|
||||
"author": "DzAvril",
|
||||
"level": 1
|
||||
@@ -458,9 +458,17 @@
|
||||
"DownloaderHelper": {
|
||||
"name": "下载器助手",
|
||||
"description": "自动做种、站点标签、自动删种。",
|
||||
"version": "1.1",
|
||||
"version": "1.4",
|
||||
"icon": "DownloaderHelper.png",
|
||||
"author": "hotlcc",
|
||||
"level": 2
|
||||
},
|
||||
"FeiShuMsg": {
|
||||
"name": "飞书机器人消息通知",
|
||||
"description": "支持使用飞书群聊机器人发送消息通知。",
|
||||
"version": "1.0",
|
||||
"icon": "FeiShu_A.png",
|
||||
"author": "InfinityPacer",
|
||||
"level": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import base64
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import json
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
from typing import Any, List, Dict, Tuple, Optional, Union, Set
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app import schemas
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.core.config import settings
|
||||
@@ -24,6 +22,7 @@ from app.plugins import _PluginBase
|
||||
from app.schemas import NotificationType, TorrentInfo
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
@@ -67,6 +66,7 @@ class BrushConfig:
|
||||
self.brush_sequential = config.get("brush_sequential", False)
|
||||
self.proxy_download = config.get("proxy_download", True)
|
||||
self.proxy_delete = config.get("proxy_delete", False)
|
||||
self.log_more = config.get("log_more", False)
|
||||
self.active_time_range = config.get("active_time_range")
|
||||
self.enable_site_config = config.get("enable_site_config", False)
|
||||
self.brush_tag = "刷流"
|
||||
@@ -117,8 +117,10 @@ class BrushConfig:
|
||||
|
||||
self.group_site_configs[sitename] = BrushConfig(config=full_config, process_site_config=False)
|
||||
except Exception as e:
|
||||
logger.error(f"解析站点配置失败,请检查配置项。错误详情: {e}")
|
||||
logger.error(f"解析站点配置失败,已停用插件并关闭站点独立配置,请检查配置项,错误详情: {e}")
|
||||
self.group_site_configs = {}
|
||||
self.enable_site_config = False
|
||||
self.enabled = False
|
||||
|
||||
def get_site_config(self, sitename):
|
||||
"""
|
||||
@@ -182,7 +184,7 @@ class BrushFlow(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "brush.jpg"
|
||||
# 插件版本
|
||||
plugin_version = "2.0"
|
||||
plugin_version = "2.2"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp,InfinityPacer"
|
||||
# 作者主页
|
||||
@@ -262,6 +264,12 @@ class BrushFlow(_PluginBase):
|
||||
brush_config.archive_task = False
|
||||
self.__update_config()
|
||||
|
||||
if brush_config.log_more:
|
||||
if brush_config.enable_site_config:
|
||||
logger.info(f"已开启站点独立配置,配置信息:{brush_config}")
|
||||
else:
|
||||
logger.info(f"没有开启站点独立配置,配置信息:{brush_config}")
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
@@ -282,7 +290,7 @@ class BrushFlow(_PluginBase):
|
||||
# 如果开启&存在站点时,才需要启用后台任务
|
||||
self._task_brush_enable = brush_config.enabled and brush_config.brushsites
|
||||
|
||||
# brush_config.onlyonce = True
|
||||
# brush_config.onlyonce = True
|
||||
|
||||
# 检查是否启用了一次性任务
|
||||
if brush_config.onlyonce:
|
||||
@@ -1030,6 +1038,27 @@ class BrushFlow(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
"content": [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'log_more',
|
||||
'label': '记录更多日志',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
@@ -1156,7 +1185,8 @@ class BrushFlow(_PluginBase):
|
||||
"proxy_delete": False,
|
||||
"freeleech": "free",
|
||||
"hr": "yes",
|
||||
"enable_site_config": False
|
||||
"enable_site_config": False,
|
||||
"log_more": False
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
@@ -1658,8 +1688,15 @@ class BrushFlow(_PluginBase):
|
||||
torrent_tasks: Dict[str, dict] = self.get_data("torrents") or {}
|
||||
torrents_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks)
|
||||
|
||||
# 判断能否通过保种体积前置条件
|
||||
size_condition_passed, reason = self.__evaluate_size_condition_for_brush(torrents_size=torrents_size)
|
||||
self.__log_brush_conditions(passed=size_condition_passed, reason=reason)
|
||||
if not size_condition_passed:
|
||||
return
|
||||
|
||||
# 判断能否通过刷流前置条件
|
||||
pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(torrents_size=torrents_size)
|
||||
pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush()
|
||||
self.__log_brush_conditions(passed=pre_condition_passed, reason=reason)
|
||||
if not pre_condition_passed:
|
||||
return
|
||||
|
||||
@@ -1695,7 +1732,9 @@ class BrushFlow(_PluginBase):
|
||||
logger.info(f"刷流任务执行完成")
|
||||
|
||||
def __brush_site_torrents(self, siteid, torrent_tasks, statistic_info) -> bool:
|
||||
|
||||
"""
|
||||
针对站点进行刷流
|
||||
"""
|
||||
siteinfo = self.siteoper.get(siteid)
|
||||
if not siteinfo:
|
||||
logger.warn(f"站点不存在:{siteid}")
|
||||
@@ -1723,23 +1762,24 @@ class BrushFlow(_PluginBase):
|
||||
# 过滤种子
|
||||
for torrent in torrents:
|
||||
# 判断能否通过刷流前置条件
|
||||
seeding_size = torrents_size + torrent.size
|
||||
pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(torrents_size=seeding_size,
|
||||
include_network_conditions=False)
|
||||
pre_condition_passed, reason = self.__evaluate_pre_conditions_for_brush(include_network_conditions=False)
|
||||
self.__log_brush_conditions(passed=pre_condition_passed, reason=reason, torrent=torrent)
|
||||
if not pre_condition_passed:
|
||||
# logger.info(f"种子没有通过刷流前置条件校验,原因:{reason} 种子:{torrent.title}|{torrent.description}")
|
||||
return False
|
||||
# else:
|
||||
# logger.info(f"种子已通过刷流前置校验,种子:{torrent.title}|{torrent.description}")
|
||||
|
||||
# 判断能否通过保种体积刷流条件
|
||||
size_condition_passed, reason = self.__evaluate_size_condition_for_brush(torrents_size=torrents_size,
|
||||
brush_torrent_size=torrent.size)
|
||||
self.__log_brush_conditions(passed=size_condition_passed, reason=reason, torrent=torrent)
|
||||
if not size_condition_passed:
|
||||
continue
|
||||
|
||||
# 判断能否通过刷流条件
|
||||
condition_passed, reason = self.__evaluate_conditions_for_brush(torrent=torrent,
|
||||
torrent_tasks=torrent_tasks)
|
||||
self.__log_brush_conditions(passed=condition_passed, reason=reason, torrent=torrent)
|
||||
if not condition_passed:
|
||||
# logger.info(f"种子没有通过刷流条件校验,原因:{reason} 种子:{torrent.title}|{torrent.description}")
|
||||
continue
|
||||
# else:
|
||||
# logger.info(f"种子已通过刷流条件校验,种子:{torrent.title}|{torrent.description}")
|
||||
|
||||
# 添加下载任务
|
||||
hash_string = self.__download(torrent=torrent)
|
||||
@@ -1790,14 +1830,43 @@ class BrushFlow(_PluginBase):
|
||||
|
||||
return True
|
||||
|
||||
def __evaluate_pre_conditions_for_brush(self, torrents_size: float,
|
||||
include_network_conditions: bool = True) -> Tuple[bool, Optional[str]]:
|
||||
def __evaluate_size_condition_for_brush(self, torrents_size: float,
|
||||
brush_torrent_size: float = 0.0) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
过滤体积不符合条件的种子
|
||||
"""
|
||||
total_size = self.__bytes_to_gb(torrents_size + brush_torrent_size) # 预计总做种体积
|
||||
|
||||
def generate_message(config):
|
||||
if brush_torrent_size > 0:
|
||||
return (f"当前做种体积 {self.__bytes_to_gb(torrents_size):.1f} GB,"
|
||||
f"刷流种子 {self.__bytes_to_gb(brush_torrent_size):.1f} GB,"
|
||||
f"预计做种体积 {total_size:.1f} GB,已超过做种体积 {config} GB")
|
||||
else:
|
||||
return f"当前做种体积 {self.__bytes_to_gb(torrents_size):.1f} GB,已超过做种体积 {config} GB,暂时停止新增任务"
|
||||
|
||||
reasons = [
|
||||
("disksize",
|
||||
lambda config: torrents_size + brush_torrent_size > float(config) * 1024 ** 3, generate_message)
|
||||
]
|
||||
|
||||
brush_config = self.__get_brush_config()
|
||||
for condition, check, message in reasons:
|
||||
config_value = getattr(brush_config, condition, None)
|
||||
if config_value and check(config_value):
|
||||
reason = message(config_value)
|
||||
return False, reason
|
||||
|
||||
return True, None
|
||||
|
||||
def __evaluate_pre_conditions_for_brush(self, include_network_conditions: bool = True) \
|
||||
-> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
前置过滤不符合条件的种子
|
||||
"""
|
||||
reasons = [
|
||||
("maxdlcount", lambda config: self.__get_downloading_count() >= int(config),
|
||||
lambda config: f"当前同时下载任务数已达到最大值 {config},暂时停止新增任务"),
|
||||
("disksize", lambda config: torrents_size > float(config) * 1024 ** 3,
|
||||
lambda config: f"当前做种体积 {self.__bytes_to_gb(torrents_size):.1f} GB,"
|
||||
f"已超过保种体积 {config} GB,暂时停止新增任务"),
|
||||
lambda config: f"当前同时下载任务数已达到最大值 {config},暂时停止新增任务")
|
||||
]
|
||||
|
||||
if include_network_conditions:
|
||||
@@ -1819,7 +1888,6 @@ class BrushFlow(_PluginBase):
|
||||
config_value = getattr(brush_config, condition, None)
|
||||
if config_value and check(config_value):
|
||||
reason = message(config_value)
|
||||
logger.warn(reason)
|
||||
return False, reason
|
||||
|
||||
return True, None
|
||||
@@ -1894,6 +1962,18 @@ class BrushFlow(_PluginBase):
|
||||
|
||||
return True, None
|
||||
|
||||
def __log_brush_conditions(self, passed: bool, reason: str, torrent: Any = None):
|
||||
"""
|
||||
记录刷流日志
|
||||
"""
|
||||
if not passed:
|
||||
if not torrent:
|
||||
logger.warn(f"种子没有通过前置刷流条件校验,原因:{reason}")
|
||||
else:
|
||||
brush_config = self.__get_brush_config()
|
||||
if brush_config.log_more:
|
||||
logger.warn(f"种子没有通过刷流条件校验,原因:{reason} 种子:{torrent.title}|{torrent.description}")
|
||||
|
||||
# endregion
|
||||
|
||||
# region Check
|
||||
@@ -1944,25 +2024,27 @@ class BrushFlow(_PluginBase):
|
||||
# 先更新刷流任务的最新状态,上下传,分享率
|
||||
self.__update_torrent_tasks_state(torrents=check_torrents, torrent_tasks=torrent_tasks)
|
||||
|
||||
# 先通过获取的全量种子,判断已经被删除,但是任务记录中还没有被标记删除的种子
|
||||
undeleted_hashes = self.__get_undeleted_torrents_missing_in_downloader(torrent_tasks, torrent_check_hashes,
|
||||
check_torrents) or []
|
||||
|
||||
# 排除MoviePilot种子
|
||||
if check_torrents and brush_config.except_tags:
|
||||
check_torrents = self.__filter_torrents_by_tag(torrents=check_torrents,
|
||||
exclude_tag=settings.TORRENT_TAG)
|
||||
|
||||
need_delete_hashes = []
|
||||
need_delete_hashes.extend(undeleted_hashes)
|
||||
# 先通过获取的全量种子,判断已经被删除,但是任务记录中还没有被标记删除的种子
|
||||
undeleted_hashes = self.__get_undeleted_torrents_missing_in_downloader(torrent_tasks, torrent_check_hashes,
|
||||
check_torrents) or []
|
||||
# 这里提前把已经被删除的种子进行标记,避免开启动态删除种子统计体积有时差
|
||||
if undeleted_hashes:
|
||||
for torrent_hash in undeleted_hashes:
|
||||
torrent_tasks[torrent_hash]["deleted"] = True
|
||||
|
||||
need_delete_hashes = []
|
||||
# 如果配置了删种阈值,则根据动态删种进行分组处理
|
||||
if brush_config.proxy_delete and brush_config.delete_size_range:
|
||||
logger.info("已开启动态删种,按系统默认动态删种条件开始检查任务")
|
||||
proxy_delete_hashs = self.__delete_torrent_for_proxy(torrents=check_torrents,
|
||||
torrent_tasks=torrent_tasks) or []
|
||||
need_delete_hashes.extend(proxy_delete_hashs)
|
||||
# 否则均认为是没有开启动态删种
|
||||
# 否则均认为是没有开启动态删种
|
||||
else:
|
||||
logger.info("没有开启动态删种,按用户设置删种条件开始检查任务")
|
||||
not_proxy_delete_hashs = self.__delete_torrent_for_evaluate_conditions(torrents=check_torrents,
|
||||
@@ -2138,9 +2220,9 @@ class BrushFlow(_PluginBase):
|
||||
|
||||
def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List:
|
||||
"""
|
||||
支持动态删除种子,当设置了动态删种(全局)和删除阈值时,当保种体积达到删除阈值时,优先按设置规则进行删除,若还没有达到阈值,则排除HR种子后按加入时间倒序进行删除
|
||||
删除阈值:100,当保种体积 > 100G 时,则开始删除种子,直至降低至 100G
|
||||
删除阈值:50-100,当保种体积 > 100G 时,则开始删除种子,直至降至为 50G
|
||||
支持动态删除种子,当设置了动态删种(全局)和删除阈值时,当做种体积达到删除阈值时,优先按设置规则进行删除,若还没有达到阈值,则排除HR种子后按加入时间倒序进行删除
|
||||
删除阈值:100,当做种体积 > 100G 时,则开始删除种子,直至降低至 100G
|
||||
删除阈值:50-100,当做种体积 > 100G 时,则开始删除种子,直至降至为 50G
|
||||
"""
|
||||
brush_config = self.__get_brush_config()
|
||||
|
||||
@@ -2150,26 +2232,27 @@ class BrushFlow(_PluginBase):
|
||||
|
||||
# 解析删除阈值范围
|
||||
sizes = [float(size) * 1024 ** 3 for size in brush_config.delete_size_range.split("-")]
|
||||
min_size = sizes[0] # 至少需要达到的保种体积
|
||||
max_size = sizes[1] if len(sizes) > 1 else sizes[0] # 触发删除操作的保种体积上限
|
||||
min_size = sizes[0] # 至少需要达到的做种体积
|
||||
max_size = sizes[1] if len(sizes) > 1 else sizes[0] # 触发删除操作的做种体积上限
|
||||
|
||||
torrent_info_map = {self.__get_hash(torrent): self.__get_torrent_info(torrent=torrent) for torrent in torrents}
|
||||
|
||||
# 计算当前总保种体积
|
||||
total_torrent_size = sum(info.get("total_size", 0) for info in torrent_info_map.values())
|
||||
# 计算当前总做种体积
|
||||
# total_torrent_size = sum(info.get("total_size", 0) for info in torrent_info_map.values())
|
||||
total_torrent_size = self.__calculate_seeding_torrents_size(torrent_tasks=torrent_tasks)
|
||||
|
||||
# 当总体积未超过最大阈值时,不需要执行删除操作
|
||||
if total_torrent_size < max_size:
|
||||
logger.info(
|
||||
f"当前保种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB,下限 {self.__bytes_to_gb(min_size):.1f} GB,未触发动态删除")
|
||||
f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB,下限 {self.__bytes_to_gb(min_size):.1f} GB,未触发动态删除")
|
||||
return []
|
||||
else:
|
||||
logger.info(
|
||||
f"当前保种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB,下限 {self.__bytes_to_gb(min_size):.1f} GB,触发动态删除")
|
||||
f"当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB,上限 {self.__bytes_to_gb(max_size):.1f} GB,下限 {self.__bytes_to_gb(min_size):.1f} GB,触发动态删除")
|
||||
|
||||
need_delete_hashes = []
|
||||
|
||||
# 即使开了动态删除,但是也有可能部分站点单独设置了关闭,这里根据种子动态进行分组,先处理不需要动态的种子,按设置的规则进行删除
|
||||
# 即使开了动态删除,但是也有可能部分站点单独设置了关闭,这里根据种子托管进行分组,先处理不需要托管的种子,按设置的规则进行删除
|
||||
proxy_delete_torrents, not_proxy_delete_torrents = self.__group_torrents_by_proxy_delete(torrents=torrents,
|
||||
torrent_tasks=torrent_tasks)
|
||||
logger.info(f"托管种子数 {len(proxy_delete_torrents)},未托管种子数 {len(not_proxy_delete_torrents)}")
|
||||
@@ -2181,7 +2264,7 @@ class BrushFlow(_PluginBase):
|
||||
torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) for torrent in not_proxy_delete_torrents
|
||||
if self.__get_hash(torrent) in not_proxy_delete_hashes)
|
||||
|
||||
# 如果删除非动态种子后仍未达到最小体积要求,则处理动态种子
|
||||
# 如果删除非托管种子后仍未达到最小体积要求,则处理托管种子
|
||||
if total_torrent_size > min_size and proxy_delete_torrents:
|
||||
proxy_delete_hashes = self.__delete_torrent_for_evaluate_conditions(torrents=proxy_delete_torrents,
|
||||
torrent_tasks=torrent_tasks,
|
||||
@@ -2191,7 +2274,7 @@ class BrushFlow(_PluginBase):
|
||||
torrent_info_map[self.__get_hash(torrent)].get("total_size", 0) for torrent in proxy_delete_torrents if
|
||||
self.__get_hash(torrent) in proxy_delete_hashes)
|
||||
|
||||
# 在完成初始删除步骤后,如果总体积仍然超过最小阈值,则进一步找到已完成种子并排除HR种子后按加入时间倒序进行删除
|
||||
# 在完成初始删除步骤后,如果总体积仍然超过最小阈值,则进一步找到已完成种子并排除HR种子后按加入时间正序进行删除
|
||||
if total_torrent_size > min_size:
|
||||
# 重新计算当前的种子列表,排除已删除的种子
|
||||
remaining_hashes = list(
|
||||
@@ -2227,7 +2310,7 @@ class BrushFlow(_PluginBase):
|
||||
reason=reason)
|
||||
logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}")
|
||||
|
||||
msg = f"已完成 {len(need_delete_hashes)} 个种子删除,当前保种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB"
|
||||
msg = f"已完成 {len(need_delete_hashes)} 个种子删除,当前做种体积 {self.__bytes_to_gb(total_torrent_size):.1f} GB"
|
||||
self.post_message(mtype=NotificationType.SiteMessage, title="【刷流任务种子删除】", text=msg)
|
||||
logger.info(msg)
|
||||
|
||||
@@ -2453,6 +2536,7 @@ class BrushFlow(_PluginBase):
|
||||
"brush_sequential": brush_config.brush_sequential,
|
||||
"proxy_download": brush_config.proxy_download,
|
||||
"proxy_delete": brush_config.proxy_delete,
|
||||
"log_more": brush_config.log_more,
|
||||
"active_time_range": brush_config.active_time_range,
|
||||
"enable_site_config": brush_config.enable_site_config,
|
||||
"site_config": brush_config.site_config
|
||||
@@ -2604,7 +2688,8 @@ class BrushFlow(_PluginBase):
|
||||
# 获取种子Hash
|
||||
torrent_hash = self.qb.get_torrent_id_by_tag(tags=tag)
|
||||
if not torrent_hash:
|
||||
logger.error(f"{brush_config.downloader} 获取种子Hash失败")
|
||||
logger.error(f"{brush_config.downloader} 获取种子Hash失败"
|
||||
f"{',请尝试开启代理下载种子' if not brush_config.proxy_download else ''}")
|
||||
return None
|
||||
return torrent_hash
|
||||
return None
|
||||
@@ -2622,7 +2707,7 @@ class BrushFlow(_PluginBase):
|
||||
else:
|
||||
logger.error('代理下载种子失败,继续尝试传递种子地址到下载器进行下载')
|
||||
if torrent_content:
|
||||
torrent = self.tr.add_torrent(content=torrent.enclosure,
|
||||
torrent = self.tr.add_torrent(content=torrent_content,
|
||||
download_dir=download_dir,
|
||||
cookie=torrent.site_cookie,
|
||||
labels=["已整理", brush_config.brush_tag])
|
||||
|
||||
@@ -39,7 +39,7 @@ class ContractCheck(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "contract.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
plugin_version = "1.1"
|
||||
# 插件作者
|
||||
plugin_author = "DzAvril"
|
||||
# 作者主页
|
||||
@@ -108,11 +108,11 @@ class ContractCheck(_PluginBase):
|
||||
)
|
||||
|
||||
self._site_schema.sort(key=lambda x: x.order)
|
||||
# 站点数据
|
||||
self._sites_data = {}
|
||||
|
||||
# 立即运行一次
|
||||
if self._onlyonce:
|
||||
# 站点数据
|
||||
self._sites_data = {}
|
||||
# 定时服务
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
logger.info(f"保种契约检查服务启动,立即运行一次")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
@@ -33,18 +34,33 @@ class CSSiteConfig(object):
|
||||
站点辅种配置类
|
||||
"""
|
||||
|
||||
def __init__(self, site_name: str, site_url: str, site_passkey: str) -> None:
|
||||
self.name = site_name
|
||||
self.url = site_url.removesuffix("/")
|
||||
self.passkey = site_passkey
|
||||
def __init__(
|
||||
self,
|
||||
name: str = None,
|
||||
url: str = None,
|
||||
passkey: str = None,
|
||||
id: int = None,
|
||||
cookie: str = None,
|
||||
ua: str = None,
|
||||
proxy: bool = None,
|
||||
query_gap: int = 1,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.passkey = passkey
|
||||
self.id = id
|
||||
self.cookie = cookie
|
||||
self.ua = ua
|
||||
self.proxy = proxy
|
||||
self.query_gap = query_gap
|
||||
|
||||
def get_api_url(self):
|
||||
if self.name == "憨憨":
|
||||
return f"{self.url}/npapi/pieces-hash"
|
||||
return f"{self.url}/api/pieces-hash"
|
||||
return f"{self.url}npapi/pieces-hash"
|
||||
return f"{self.url}api/pieces-hash"
|
||||
|
||||
def get_torrent_url(self, torrent_id: str):
|
||||
return f"{self.url}/download.php?id={torrent_id}&passkey={self.passkey}"
|
||||
return f"{self.url}download.php?id={torrent_id}&passkey={self.passkey}"
|
||||
|
||||
|
||||
class TorInfo:
|
||||
@@ -135,21 +151,21 @@ class CrossSeedHelper(object):
|
||||
"User-Agent": "CrossSeedHelper",
|
||||
}
|
||||
data = {"passkey": site.passkey, "pieces_hash": pieces_hash_set}
|
||||
remote_torrent_infos = []
|
||||
try:
|
||||
response = requests.post(
|
||||
site.get_api_url(), headers=headers, json=data, timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
rsp_body = response.json()
|
||||
if isinstance(rsp_body["data"], dict):
|
||||
for pieces_hash, torrent_id in rsp_body["data"].items():
|
||||
remote_torrent_infos.append(
|
||||
TorInfo.remote(site.name, pieces_hash, torrent_id)
|
||||
)
|
||||
time.sleep(site.query_gap)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f"站点{site.name}请求失败:{e}"
|
||||
rsp_body = response.json()
|
||||
|
||||
remote_torrent_infos = []
|
||||
if isinstance(rsp_body["data"], dict):
|
||||
for pieces_hash, torrent_id in rsp_body["data"].items():
|
||||
remote_torrent_infos.append(
|
||||
TorInfo.remote(site.name, pieces_hash, torrent_id)
|
||||
)
|
||||
return remote_torrent_infos, None
|
||||
|
||||
|
||||
@@ -157,11 +173,11 @@ class CrossSeed(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "青蛙辅种助手"
|
||||
# 插件描述
|
||||
plugin_desc = "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音等。"
|
||||
plugin_desc = "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。"
|
||||
# 插件图标
|
||||
plugin_icon = "qingwa.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.8"
|
||||
plugin_version = "2.0"
|
||||
# 插件作者
|
||||
plugin_author = "233@qingwa"
|
||||
# 作者主页
|
||||
@@ -206,7 +222,6 @@ class CrossSeed(_PluginBase):
|
||||
# 辅种缓存,出错的种子不再重复辅种,且无法清除。种子被删除404等情况
|
||||
_permanent_error_caches = []
|
||||
_torrentpaths = []
|
||||
_name_site_map = {}
|
||||
_site_cs_infos = []
|
||||
# 辅种计数
|
||||
total = 0
|
||||
@@ -229,7 +244,7 @@ class CrossSeed(_PluginBase):
|
||||
|
||||
self._downloaders = config.get("downloaders")
|
||||
self._torrentpath = config.get("torrentpath") # 种子路径和下载器对应 /qb,/tr
|
||||
self._torrentpaths = self._torrentpath.split(",")
|
||||
self._torrentpaths = self._torrentpath.strip().split(",")
|
||||
self._sites = config.get("sites") or []
|
||||
self._notify = config.get("notify")
|
||||
self._nolabels = config.get("nolabels")
|
||||
@@ -247,18 +262,61 @@ class CrossSeed(_PluginBase):
|
||||
self._sites = [site_id for site_id, site_name in all_sites if site_id in self._sites]
|
||||
# 拆分出选中的站点
|
||||
site_names = [site_name for site_id, site_name in all_sites if site_id in self._sites]
|
||||
# 拆分为映射关系
|
||||
self._name_site_map = {}
|
||||
for site in self.siteoper.list_order_by_pri():
|
||||
self._name_site_map[site.name] = site
|
||||
# 只给选中的站点构造站点配置
|
||||
self._site_cs_infos: List[CSSiteConfig] = []
|
||||
|
||||
# 整理所有可用内部站点信息
|
||||
all_site_cs_info_map : dict[str, CSSiteConfig] = dict()
|
||||
for site in inner_site_list:
|
||||
if site.is_active:
|
||||
all_site_cs_info_map[site.name] = CSSiteConfig(
|
||||
name=site.name,
|
||||
url=site.url,
|
||||
id=site.id,
|
||||
cookie=site.cookie,
|
||||
ua=site.ua,
|
||||
proxy=site.proxy,
|
||||
)
|
||||
for site in self.__custom_sites():
|
||||
all_site_cs_info_map[site.get("name")] = CSSiteConfig(
|
||||
name=site.get("name"),
|
||||
url=site.get("url"),
|
||||
id=site.get("id"),
|
||||
cookie=site.get("cookie"),
|
||||
ua=site.get("ua"),
|
||||
proxy=site.get("proxy"),
|
||||
)
|
||||
self._sites = [site.id for site in all_site_cs_info_map.values() if site.id in self._sites]
|
||||
site_names = [site.name for site in all_site_cs_info_map.values() if site.id in self._sites]
|
||||
|
||||
# 整理passkey映射关系
|
||||
site_name_key_map = dict()
|
||||
site_name_gap_map = dict()
|
||||
for site_key in self._token.strip().split("\n"):
|
||||
site_key_arr = re.split("[\s::]+",site_key.strip())
|
||||
site_name = site_key_arr[0]
|
||||
db_site = self._name_site_map[site_name]
|
||||
if site_name in site_names and db_site:
|
||||
self._site_cs_infos.append(CSSiteConfig(site_name, db_site.url, site_key_arr[1]))
|
||||
site_name_key_map[site_name] = site_key_arr[1]
|
||||
if len(site_key_arr) > 2:
|
||||
if str.isdigit(site_key_arr[2]):
|
||||
site_name_gap_map[site_name] = int(site_key_arr[2])
|
||||
else:
|
||||
logger.warn(
|
||||
f"站点{site_name}配置的查询请求间隔时间不为整数,不能生效, 请修改 {site_key_arr[2]}"
|
||||
)
|
||||
|
||||
# 只给选中的站点补全站点配置
|
||||
self._site_cs_infos: List[CSSiteConfig] = []
|
||||
# 根据配置来补充passkey
|
||||
for site_name in site_names:
|
||||
site_key = site_name_key_map.get(site_name)
|
||||
if not site_key:
|
||||
logger.warning(f"未找到站点{site_name}的passkey, 请检查passkey配置是否有误,站点{site_name}将跳过辅种")
|
||||
continue
|
||||
site_cs_info = all_site_cs_info_map.get(site_name)
|
||||
site_cs_info.passkey = site_key
|
||||
# 追加设置的请求间隔时间
|
||||
site_query_gap = site_name_gap_map.get(site_name)
|
||||
if site_query_gap:
|
||||
site_cs_info.query_gap = site_query_gap
|
||||
self._site_cs_infos.append(site_cs_info)
|
||||
|
||||
self.__update_config()
|
||||
|
||||
@@ -611,7 +669,7 @@ class CrossSeed(_PluginBase):
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '1. 定时任务周期建议每次辅种间隔时间大于1天,不填写每天上午2点到7点随机辅种一次; '
|
||||
'2. 支持辅种站点列表:青蛙【已验证】,AGSVPT,麒麟,UBits,聆音 等,配置passkey时,站点名称需严格和上面选项一致,只有选中的站点会辅种,passkey可保存多个; '
|
||||
'2. 支持辅种站点列表:青蛙、AGSVPT、红豆饭、麒麟、UBits、聆音等,配置passkey时,站点名称需严格和上面选项一致,只有选中的站点会辅种,passkey可保存多个; '
|
||||
'3. 请勿与IYUU辅种插件同时添加相同站点,可能会有冲突,且意义不大;'
|
||||
'4. 测试站点是否支持的方法:【站点域名/api/pieces-hash】接口访问返回405则大概率支持 '
|
||||
}
|
||||
@@ -619,6 +677,29 @@ class CrossSeed(_PluginBase):
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '【进阶设置】如果辅种过程中访问/api/pieces-hash接口偶尔会失败,可以设置请求间隔时间。 '
|
||||
'可以在passkey后增加 :3 来将某个站点的请求间隔调整为3秒,3可以改为其他数字,只能为整数,默认请求间隔为1秒。 '
|
||||
'示例配置 站点名称:Passkey:3'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -709,14 +790,14 @@ class CrossSeed(_PluginBase):
|
||||
if not torrent_path.exists():
|
||||
if downloader == "qbittorrent":
|
||||
# FIXME qb从4.4.0开始,种子文件以标题+序号的方式保存,目前只能尝试导出后再解析
|
||||
# logger.info(f"正在导出种子 {torrent.get('name')}({hash_str})")
|
||||
logger.warn(f"QB种子文件不存在:{torrent_path} 尝试远程导出种子")
|
||||
try:
|
||||
torrent_data = torrent.export()
|
||||
torrent_info, err = TorInfo.from_data(torrent_data)
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
if not torrent_info:
|
||||
logger.error(f"尝试导出种子 {hash_str} 出错 {err}")
|
||||
logger.error(f"尝试远程导出种子 {hash_str} 出错 {err}")
|
||||
continue
|
||||
else:
|
||||
logger.error(f"种子文件不存在:{torrent_path}")
|
||||
@@ -873,12 +954,12 @@ class CrossSeed(_PluginBase):
|
||||
# 逐个站点查询可辅种数据
|
||||
chunk_size = 100
|
||||
for site_config in self._site_cs_infos:
|
||||
db_site_info = self._name_site_map[site_config.name]
|
||||
if not db_site_info:
|
||||
logger.info(f"未在支持站点中找到{site_config.name}")
|
||||
remote_tors: List[TorInfo] = []
|
||||
total_size = len(pieces_hashes)
|
||||
for i in range(0, len(pieces_hashes), chunk_size):
|
||||
if self._event.is_set():
|
||||
logger.info(f"辅种服务停止")
|
||||
return
|
||||
# 切片操作
|
||||
chunk = pieces_hashes[i:i + chunk_size]
|
||||
# 处理分组
|
||||
@@ -911,6 +992,9 @@ class CrossSeed(_PluginBase):
|
||||
logger.info(f"站点{site_config.name}正在做种或已经辅种过的种子数为{local_cnt}")
|
||||
|
||||
for tor_info in not_local_tors:
|
||||
if self._event.is_set():
|
||||
logger.info(f"辅种服务停止")
|
||||
return
|
||||
if not tor_info:
|
||||
continue
|
||||
if not tor_info.torrent_id or not tor_info.pieces_hash:
|
||||
@@ -922,7 +1006,7 @@ class CrossSeed(_PluginBase):
|
||||
logger.info(f"种子 {tor_info.get_name_id_tag()} 辅种失败且已缓存,跳过 ...")
|
||||
continue
|
||||
# 添加任务
|
||||
self.__download_torrent(tor=tor_info, site_config=site_config, site_info=db_site_info,
|
||||
self.__download_torrent(tor=tor_info, site_config=site_config,
|
||||
downloader=downloader,
|
||||
save_path=save_paths.get(tor_info.pieces_hash))
|
||||
|
||||
@@ -967,7 +1051,6 @@ class CrossSeed(_PluginBase):
|
||||
self,
|
||||
tor: TorInfo,
|
||||
site_config: CSSiteConfig,
|
||||
site_info: Site,
|
||||
downloader: str,
|
||||
save_path: str,
|
||||
):
|
||||
@@ -984,9 +1067,9 @@ class CrossSeed(_PluginBase):
|
||||
# 下载种子文件
|
||||
_, content, _, _, error_msg = self.torrent.download_torrent(
|
||||
url=torrent_url,
|
||||
cookie=site_info.cookie,
|
||||
ua=site_info.ua or settings.USER_AGENT,
|
||||
proxy=True if site_info.proxy else False)
|
||||
cookie=site_config.cookie,
|
||||
ua=site_config.ua or settings.USER_AGENT,
|
||||
proxy=True if site_config.proxy else False)
|
||||
|
||||
# 兼容种子无法访问的情况
|
||||
if not content or (isinstance(content, bytes) and "你没有该权限".encode(encoding="utf-8") in content):
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|定时执行周期|插件定时服务的cron表达式,仅支持5位的,缺省时不注册定时服务。|
|
||||
|排除种子标签|多个标签通过英文逗号分割,具备配置的任意标签的种子不会进行自动做种、站点标签、自动删种操作。|
|
||||
|站点标签前缀|站点标签的前缀,缺省时不添加前缀。|
|
||||
|Tracker映射|站点标签的原理是根据tracker的域名去匹配站点,但是有的PT站的tracker域名和站点域名不一致,导致匹配不到站点,因此需要对这些特殊站点的tracker做映射;每行一个映射,格式是 `tracker域名:站点域名`,tracker域名至少需要配置二级,站点域名只能配置二级。|
|
||||
|Tracker映射|站点标签的原理是根据tracker的域名去匹配站点,但是有的PT站的tracker域名和站点域名不一致,导致匹配不到站点,因此需要对这些特殊站点的tracker做映射;每行一个映射,格式是 `tracker域名:站点域名`,tracker域名可以是完整域名或者主域名。|
|
||||
|
||||
##### 2.1.2、下载器子任务配置项
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class DownloaderHelper(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "DownloaderHelper.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1"
|
||||
plugin_version = "1.4"
|
||||
# 插件作者
|
||||
plugin_author = "hotlcc"
|
||||
# 作者主页
|
||||
@@ -77,6 +77,8 @@ class DownloaderHelper(_PluginBase):
|
||||
__tracker_mappings: Dict[str, str] = {}
|
||||
# 排除种子标签
|
||||
__exclude_tags: Set[str] = set()
|
||||
# 多级根域名,用于在打标时做特殊处理
|
||||
__multi_level_root_domain: List[str] = ['edu.cn', 'com.cn', 'net.cn', 'org.cn']
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
"""
|
||||
@@ -175,87 +177,90 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '插件总开关'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enable',
|
||||
'label': '启用插件'
|
||||
'label': '启用插件',
|
||||
'hint': '插件总开关'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '执行插件任务后是否发送通知'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enable_notify',
|
||||
'label': '发送通知'
|
||||
'label': '发送通知',
|
||||
'hint': '执行插件任务后是否发送通知'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '保存插件配置后是否立即触发一次插件任务运行'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'run_once',
|
||||
'label': '立即运行一次'
|
||||
'label': '立即运行一次',
|
||||
'hint': '保存插件配置后是否立即触发一次插件任务运行'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
'component': 'VRow',
|
||||
'content': [{
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '监听下载添加事件。当MoviePilot添加下载任务时,会触发执行本插件进行自动做种和添加站点标签。'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'listen_download_event',
|
||||
'label': '监听下载事件'
|
||||
'label': '监听下载事件',
|
||||
'hint': '监听下载添加事件。当MoviePilot添加下载任务时,会触发执行本插件进行自动做种和添加站点标签。'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '监听源文件删除事件。当在【历史记录】中删除源文件时,会自动触发运行本插件任务进行自动删种。'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'listen_source_file_event',
|
||||
'label': '监听源文件事件'
|
||||
'label': '监听源文件事件',
|
||||
'hint': '监听源文件删除事件。当在【历史记录】中删除源文件时,会自动触发运行本插件任务进行自动删种。'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '给种子添加站点标签时,是否优先以站点名称作为标签内容(否则将使用域名关键字)?'
|
||||
'md': 4,
|
||||
'xl': 3
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'site_name_priority',
|
||||
'label': '站点名称优先'
|
||||
'label': '站点名称优先',
|
||||
'hint': '给种子添加站点标签时,是否优先以站点名称作为标签内容(否则将使用域名关键字)?'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -265,44 +270,44 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '设置插件任务执行周期。支持5位cron表达式,应避免任务执行过于频繁,例如:0/30 * * * *。缺省时不执行定时任务,但不影响监听任务的执行。'
|
||||
'md': 4
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '定时执行周期',
|
||||
'placeholder': '0/30 * * * *'
|
||||
'placeholder': '0/30 * * * *',
|
||||
'hint': '设置插件任务执行周期。支持5位cron表达式,应避免任务执行过于频繁,例如:0/30 * * * *。缺省时不执行定时任务,但不影响监听任务的执行。'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '下载器中的种子有这些标签时不进行任何操作,多个标签使用英文“,”分割'
|
||||
'md': 4
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'exclude_tags',
|
||||
'label': '排除种子标签'
|
||||
'label': '排除种子标签',
|
||||
'hint': '下载器中的种子有这些标签时不进行任何操作,多个标签使用英文“,”分割'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12,
|
||||
'title': '给种子添加站点标签时的标签前缀,默认值为“站点/”'
|
||||
'md': 4
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'tag_prefix',
|
||||
'label': '站点标签前缀',
|
||||
'placeholder': '站点/'
|
||||
'placeholder': '站点/',
|
||||
'hint': '给种子添加站点标签时的标签前缀,默认值为“站点/”'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -311,8 +316,7 @@ class DownloaderHelper(_PluginBase):
|
||||
'content': [{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'title': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(二级或多级),中间是英文冒号,后面是站点域名(只能是二级)。'
|
||||
'cols': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextarea',
|
||||
@@ -322,7 +326,8 @@ class DownloaderHelper(_PluginBase):
|
||||
'placeholder': '格式:\n'
|
||||
'<tracker-domain>:<site-domain>\n'
|
||||
'例如:\n'
|
||||
'chdbits.xyz:ptchdbits.co'
|
||||
'chdbits.xyz:ptchdbits.co',
|
||||
'hint': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -371,7 +376,8 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'qb_enable',
|
||||
'label': '任务开关'
|
||||
'label': '任务开关',
|
||||
'hint': '该下载器子任务的开关'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
@@ -384,7 +390,8 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'qb_enable_seeding',
|
||||
'label': '自动做种'
|
||||
'label': '自动做种',
|
||||
'hint': '是否开启自动做种功能'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
@@ -397,7 +404,8 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'qb_enable_tagging',
|
||||
'label': '站点标签'
|
||||
'label': '站点标签',
|
||||
'hint': '是否开启站点标签功能'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
@@ -410,7 +418,8 @@ class DownloaderHelper(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'qb_enable_delete',
|
||||
'label': '自动删种'
|
||||
'label': '自动删种',
|
||||
'hint': '是否开启自动删种功能'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
@@ -516,8 +525,7 @@ class DownloaderHelper(_PluginBase):
|
||||
finally:
|
||||
self.__exit_event.clear()
|
||||
|
||||
@staticmethod
|
||||
def __parse_tracker_mappings(tracker_mappings: str) -> Dict[str, str]:
|
||||
def __parse_tracker_mappings(self, tracker_mappings: str) -> Dict[str, str]:
|
||||
"""
|
||||
解析配置的tracker映射
|
||||
:param tracker_mappings: 配置的tracker映射
|
||||
@@ -540,7 +548,7 @@ class DownloaderHelper(_PluginBase):
|
||||
key, value = key.strip(), value.strip()
|
||||
if not key or not value:
|
||||
continue
|
||||
if len(key.split('.')) >= 2 and len(value.split('.')) == 2:
|
||||
if self.__is_valid_domain(key) and self.__is_valid_domain(value):
|
||||
mappings[key] = value
|
||||
return mappings
|
||||
|
||||
@@ -767,34 +775,60 @@ class DownloaderHelper(_PluginBase):
|
||||
scheme, netloc = StringUtils.get_url_netloc(url)
|
||||
return netloc
|
||||
|
||||
@staticmethod
|
||||
def __get_domain_level2(domain: str) -> Optional[str]:
|
||||
def __get_main_domain(self, domain: str) -> Optional[str]:
|
||||
"""
|
||||
获取域名的二级域名
|
||||
获取域名的主域名
|
||||
:param domain: 原域名
|
||||
:return: 主域名
|
||||
"""
|
||||
if not domain:
|
||||
return None
|
||||
domain_arr = domain.split('.')
|
||||
domain_arr_len = len(domain_arr)
|
||||
if domain_arr_len == 2:
|
||||
return domain
|
||||
elif domain_arr_len > 2:
|
||||
return f'{domain_arr[-2]}.{domain_arr[-1]}'
|
||||
else:
|
||||
domain_len = len(domain_arr)
|
||||
if domain_len < 2:
|
||||
return None
|
||||
root_domain, root_domain_len = self.__match_multi_level_root_domain(domain=domain)
|
||||
if root_domain:
|
||||
return f'{domain_arr[-root_domain_len - 1]}.{root_domain}'
|
||||
else:
|
||||
return f'{domain_arr[-2]}.{domain_arr[-1]}'
|
||||
|
||||
@staticmethod
|
||||
def __get_domain_keyword(domain: str) -> Optional[str]:
|
||||
def __get_domain_keyword(self, domain: str) -> Optional[str]:
|
||||
"""
|
||||
获取域名关键字
|
||||
"""
|
||||
main_domain = self.__get_main_domain(domain=domain)
|
||||
if not main_domain:
|
||||
return None
|
||||
return main_domain.split('.')[0]
|
||||
|
||||
def __match_multi_level_root_domain(self, domain: str) -> Tuple[Optional[str], int]:
|
||||
"""
|
||||
匹配多级根域名
|
||||
:param domain: 被匹配的域名
|
||||
:return: 匹配的根域名, 匹配的根域名长度
|
||||
"""
|
||||
if not domain or not self.__multi_level_root_domain:
|
||||
return None, 0
|
||||
for root_domain in self.__multi_level_root_domain:
|
||||
if domain.endswith('.' + root_domain):
|
||||
root_domain_len = len(root_domain.split('.'))
|
||||
return root_domain, root_domain_len
|
||||
return None, 0
|
||||
|
||||
def __is_valid_domain(self, domain: str) -> bool:
|
||||
"""
|
||||
判断域名是否有效
|
||||
:param domain: 被判断的域名
|
||||
:return: 是否有效
|
||||
"""
|
||||
if not domain:
|
||||
return None
|
||||
domain_arr = domain.split('.')
|
||||
if len(domain_arr) >= 2:
|
||||
return domain_arr[-2]
|
||||
else:
|
||||
return None
|
||||
return False
|
||||
domain_len = len(domain.split('.'))
|
||||
root_domain, root_domain_len = self.__match_multi_level_root_domain(domain)
|
||||
if root_domain:
|
||||
return domain_len > root_domain_len
|
||||
return domain_len > 1
|
||||
|
||||
def __generate_site_tag(self, site: str) -> Optional[str]:
|
||||
"""
|
||||
@@ -816,7 +850,7 @@ class DownloaderHelper(_PluginBase):
|
||||
return None, None
|
||||
|
||||
# tracker的完整域名
|
||||
tracker_domain = self.__get_url_domain(tracker_url)
|
||||
tracker_domain = self.__get_url_domain(url=tracker_url)
|
||||
if not tracker_domain:
|
||||
return None, None
|
||||
|
||||
@@ -824,20 +858,20 @@ class DownloaderHelper(_PluginBase):
|
||||
delete_suggest = set()
|
||||
|
||||
# tracker域名关键字
|
||||
tracker_domain_keyword = self.__get_domain_keyword(tracker_domain)
|
||||
tracker_domain_keyword = self.__get_domain_keyword(domain=tracker_domain)
|
||||
if tracker_domain_keyword:
|
||||
# 建议移除
|
||||
delete_suggest.add(tracker_domain_keyword)
|
||||
delete_suggest.add(self.__generate_site_tag(tracker_domain_keyword))
|
||||
delete_suggest.add(self.__generate_site_tag(site=tracker_domain_keyword))
|
||||
|
||||
# 首先根据tracker的完整域名去匹配站点信息
|
||||
site_info = self.__get_site_info_by_domain(tracker_domain)
|
||||
site_info = self.__get_site_info_by_domain(site_domain=tracker_domain)
|
||||
|
||||
# 如果没有匹配到,再根据二级域名去匹配
|
||||
# 如果没有匹配到,再根据主域名去匹配
|
||||
if not site_info:
|
||||
tracker_domain_level2 = self.__get_domain_level2(tracker_domain)
|
||||
if tracker_domain_level2:
|
||||
site_info = self.__get_site_info_by_domain(tracker_domain_level2)
|
||||
tracker_main_domain = self.__get_main_domain(domain=tracker_domain)
|
||||
if tracker_main_domain and tracker_main_domain != tracker_domain:
|
||||
site_info = self.__get_site_info_by_domain(tracker_main_domain)
|
||||
|
||||
# 如果还是没有匹配到,就根据tracker映射的域名匹配
|
||||
matched_site_domain = None
|
||||
@@ -872,7 +906,7 @@ class DownloaderHelper(_PluginBase):
|
||||
else:
|
||||
site_tag = self.__generate_site_tag(self.__get_domain_keyword(tracker_domain))
|
||||
|
||||
if site_tag:
|
||||
if site_tag and site_tag in delete_suggest:
|
||||
delete_suggest.remove(site_tag)
|
||||
|
||||
return site_tag, delete_suggest
|
||||
@@ -1444,7 +1478,8 @@ class DownloaderHelper(_PluginBase):
|
||||
# 移除建议删除的标签
|
||||
if delete_suggest and len(delete_suggest) > 0:
|
||||
for to_delete in delete_suggest:
|
||||
torrent_tags_copy.remove(to_delete)
|
||||
if to_delete and to_delete in torrent_tags_copy:
|
||||
torrent_tags_copy.remove(to_delete)
|
||||
# 如果本次需要打标签
|
||||
if site_tag and site_tag not in torrent_tags_copy:
|
||||
torrent_tags_copy.append(site_tag)
|
||||
@@ -1513,12 +1548,12 @@ class DownloaderHelper(_PluginBase):
|
||||
# 执行
|
||||
logger.info('下载添加事件监听任务执行开始...')
|
||||
context = TaskContext().enable_seeding(False).enable_tagging(True).enable_delete(False)
|
||||
hash_str = event.event_data.get('hash')
|
||||
if hash:
|
||||
context.select_torrent(hash_str)
|
||||
_hash = event.event_data.get('hash')
|
||||
if _hash:
|
||||
context.select_torrent(torrent=_hash)
|
||||
username = event.event_data.get('username')
|
||||
if username:
|
||||
context.select_username(username)
|
||||
context.set_username(username=username)
|
||||
self.__run_for_all(context=context)
|
||||
logger.info('下载添加事件监听任务执行结束')
|
||||
|
||||
|
||||
263
plugins/feishumsg/__init__.py
Normal file
263
plugins/feishumsg/__init__.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class FeiShuMsg(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "飞书机器人消息通知"
|
||||
# 插件描述
|
||||
plugin_desc = "支持使用飞书群聊机器人发送消息通知。"
|
||||
# 插件图标
|
||||
plugin_icon = "FeiShu_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "InfinityPacer"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/InfinityPacer"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "feishu_"
|
||||
# 加载顺序
|
||||
plugin_order = 28
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
_webhookurl = None
|
||||
_msgtypes = []
|
||||
_secret = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._webhookurl = config.get("webhookurl")
|
||||
self._msgtypes = config.get("msgtypes") or []
|
||||
self._secret = config.get("secret")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled and (True if self._webhookurl 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、数据结构
|
||||
"""
|
||||
# 遍历 NotificationType 枚举,生成消息类型选项
|
||||
msg_type_options = []
|
||||
default_msg_type_values = []
|
||||
for item in NotificationType:
|
||||
msg_type_options.append({
|
||||
"title": item.value,
|
||||
"value": item.name
|
||||
})
|
||||
default_msg_type_values.append(item.name)
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'webhookurl',
|
||||
'label': 'WebHook地址',
|
||||
'placeholder': 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxx',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'secret',
|
||||
'label': '密钥',
|
||||
'placeholder': '如设置了签名校验,请输入密钥',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'multiple': True,
|
||||
'chips': True,
|
||||
'model': 'msgtypes',
|
||||
'label': '消息类型',
|
||||
'items': msg_type_options
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
'webhookurl': '',
|
||||
'msgtypes': default_msg_type_values,
|
||||
'secret': '',
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
@eventmanager.register(EventType.NoticeMessage)
|
||||
def send(self, event: Event):
|
||||
"""
|
||||
消息发送事件
|
||||
"""
|
||||
if not self.get_state():
|
||||
return
|
||||
|
||||
if not event.event_data:
|
||||
return
|
||||
|
||||
msg_body = event.event_data
|
||||
# 渠道
|
||||
channel = msg_body.get("channel")
|
||||
if channel:
|
||||
logger.info(f"channel: {channel} 不进行消息推送")
|
||||
return
|
||||
# 类型
|
||||
msg_type: NotificationType = msg_body.get("type")
|
||||
# 标题
|
||||
title = msg_body.get("title")
|
||||
# 文本
|
||||
text = msg_body.get("text")
|
||||
# 图像
|
||||
image = msg_body.get("image")
|
||||
|
||||
if not title and not text:
|
||||
logger.warn("标题和内容不能同时为空")
|
||||
return
|
||||
|
||||
if (msg_type and self._msgtypes
|
||||
and msg_type.name not in self._msgtypes):
|
||||
logger.info(f"消息类型 {msg_type.value} 未开启消息发送")
|
||||
return
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"msg_type": "post",
|
||||
"content": {
|
||||
"post": {
|
||||
"zh_cn": {
|
||||
"title": title,
|
||||
"content": [
|
||||
[{
|
||||
"tag": "text",
|
||||
"text": text
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 如果存在密钥时,还需要进行签名处理
|
||||
if self._secret:
|
||||
timestamp = str(int(time.time()))
|
||||
sign = self.gen_sign(timestamp, self._secret)
|
||||
payload.update({
|
||||
"timestamp": timestamp,
|
||||
"sign": sign
|
||||
})
|
||||
|
||||
res = RequestUtils(content_type="application/json").post_res(url=self._webhookurl, json=payload)
|
||||
if res and res.status_code == 200:
|
||||
ret_json = res.json()
|
||||
errno = ret_json.get('code')
|
||||
error = ret_json.get('msg')
|
||||
if errno == 0:
|
||||
logger.info("飞书机器人消息发送成功")
|
||||
else:
|
||||
logger.warn(f"飞书机器人消息发送失败,错误码:{errno},错误原因:{error}")
|
||||
elif res is not None:
|
||||
logger.warn(f"飞书机器人消息发送失败,错误码:{res.status_code},错误原因:{res.reason}")
|
||||
else:
|
||||
logger.warn("飞书机器人消息发送失败,未获取到返回信息")
|
||||
except Exception as msg_e:
|
||||
logger.error(f"飞书机器人消息发送失败,{str(msg_e)}")
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def gen_sign(timestamp, secret):
|
||||
# 拼接timestamp和secret
|
||||
string_to_sign = '{}\n{}'.format(timestamp, secret)
|
||||
hmac_code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()
|
||||
# 对结果进行base64处理
|
||||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||||
return sign
|
||||
@@ -1 +1 @@
|
||||
roman~=4.1
|
||||
roman~=4.1
|
||||
|
||||
Reference in New Issue
Block a user