Merge branch 'main' into main

This commit is contained in:
thsrite
2024-04-28 18:21:21 +08:00
committed by GitHub
4 changed files with 1254 additions and 35 deletions

View File

@@ -2,6 +2,7 @@
"AutoSignIn": {
"name": "站点自动签到",
"description": "自动模拟登录、签到站点。",
"labels": "站点",
"version": "2.1",
"icon": "signin.png",
"author": "thsrite",
@@ -15,6 +16,7 @@
"CustomSites": {
"name": "自定义站点",
"description": "增加自定义站点为签到和统计使用。",
"labels": "站点",
"version": "1.0",
"icon": "world.png",
"author": "lightolly",
@@ -23,6 +25,7 @@
"SiteStatistic": {
"name": "站点数据统计",
"description": "自动统计和展示站点数据。",
"labels": "站点",
"version": "2.9",
"icon": "statistic.png",
"author": "lightolly",
@@ -37,6 +40,7 @@
"SiteRefresh": {
"name": "站点自动更新",
"description": "使用浏览器模拟登录站点获取Cookie和UA。",
"labels": "站点",
"version": "1.2",
"icon": "Chrome_A.png",
"author": "thsrite",
@@ -45,6 +49,7 @@
"DoubanSync": {
"name": "豆瓣想看",
"description": "同步豆瓣想看数据,自动添加订阅。",
"labels": "订阅",
"version": "1.7",
"icon": "douban.png",
"author": "jxxghp",
@@ -58,6 +63,7 @@
"DirMonitor": {
"name": "目录监控",
"description": "监控目录文件发生变化时实时整理到媒体库。",
"labels": "文件整理",
"version": "2.0",
"icon": "directory.png",
"author": "jxxghp",
@@ -70,6 +76,7 @@
"ChineseSubFinder": {
"name": "ChineseSubFinder",
"description": "整理入库时通知ChineseSubFinder下载字幕。",
"labels": "字幕",
"version": "1.1",
"icon": "chinesesubfinder.png",
"author": "jxxghp",
@@ -78,6 +85,7 @@
"DoubanRank": {
"name": "豆瓣榜单订阅",
"description": "监控豆瓣热门榜单,自动添加订阅。",
"labels": "订阅",
"version": "1.9",
"icon": "movie.jpg",
"author": "jxxghp",
@@ -90,6 +98,7 @@
"LibraryScraper": {
"name": "媒体库刮削",
"description": "定时对媒体库进行刮削,补齐缺失元数据和图片。",
"labels": "刮削",
"version": "1.4",
"icon": "scraper.png",
"author": "jxxghp",
@@ -98,6 +107,7 @@
"TorrentRemover": {
"name": "自动删种",
"description": "自动删除下载器中的下载任务。",
"labels": "做种",
"version": "1.2.2",
"icon": "delete.jpg",
"author": "jxxghp",
@@ -106,6 +116,7 @@
"MediaSyncDel": {
"name": "媒体文件同步删除",
"description": "同步删除历史记录、源文件和下载任务。",
"labels": "文件整理",
"version": "1.4",
"icon": "mediasyncdel.png",
"author": "thsrite",
@@ -114,6 +125,7 @@
"CustomHosts": {
"name": "自定义Hosts",
"description": "修改系统hosts文件加速网络访问。",
"labels": "网络",
"version": "1.0",
"icon": "hosts.png",
"author": "thsrite",
@@ -122,6 +134,7 @@
"SpeedLimiter": {
"name": "播放限速",
"description": "外网播放媒体库视频时,自动对下载器进行限速。",
"labels": "网络",
"version": "1.1",
"icon": "Librespeed_A.png",
"author": "Shurelol",
@@ -130,6 +143,7 @@
"CloudflareSpeedTest": {
"name": "Cloudflare IP优选",
"description": "🌩 测试 Cloudflare CDN 延迟和速度自动优选IP。",
"labels": "网络,站点",
"version": "1.2",
"icon": "cloudflare.jpg",
"author": "thsrite",
@@ -141,6 +155,7 @@
"BestFilmVersion": {
"name": "收藏洗版",
"description": "Jellyfin/Emby/Plex点击收藏电影后自动订阅洗版。",
"labels": "订阅",
"version": "2.1",
"icon": "like.jpg",
"author": "wlj",
@@ -149,6 +164,7 @@
"MediaServerMsg": {
"name": "媒体库服务器通知",
"description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。",
"labels": "消息通知,媒体库",
"version": "1.1",
"icon": "mediaplay.png",
"author": "jxxghp",
@@ -157,6 +173,7 @@
"MediaServerRefresh": {
"name": "媒体库服务器刷新",
"description": "入库后自动刷新Emby/Jellyfin/Plex服务器海报墙。",
"labels": "媒体库",
"version": "1.2",
"icon": "refresh2.png",
"author": "jxxghp",
@@ -173,6 +190,7 @@
"ChatGPT": {
"name": "ChatGPT",
"description": "消息交互支持与ChatGPT对话。",
"labels": "消息通知,识别",
"version": "1.3",
"icon": "Chatgpt_A.png",
"author": "jxxghp",
@@ -189,6 +207,7 @@
"MessageForward": {
"name": "消息转发",
"description": "根据正则转发通知到其他WeChat应用。",
"labels": "消息通知",
"version": "1.1",
"icon": "forward.png",
"author": "thsrite",
@@ -197,6 +216,7 @@
"AutoBackup": {
"name": "自动备份",
"description": "自动备份数据和配置文件。",
"labels": "系统设置",
"version": "1.2",
"icon": "Time_machine_B.png",
"author": "thsrite",
@@ -208,14 +228,19 @@
"IYUUAutoSeed": {
"name": "IYUU自动辅种",
"description": "基于IYUU官方Api实现自动辅种。",
"version": "1.3",
"labels": "做种,IYUU",
"version": "1.5",
"icon": "IYUU.png",
"author": "jxxghp",
"level": 2
"level": 2,
"history": {
"v1.5": "支持馒头新架构辅种"
}
},
"CrossSeed": {
"name": "青蛙辅种助手",
"description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种支持站点青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。",
"labels": "做种",
"version": "2.3",
"icon": "qingwa.png",
"author": "233@qingwa",
@@ -228,6 +253,7 @@
"VCBAnimeMonitor": {
"name": "整理VCB动漫压制组作品",
"description": "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理",
"labels": "文件整理,识别",
"version": "1.7",
"icon": "vcbmonitor.png",
"author": "pixel@qingwa",
@@ -236,6 +262,7 @@
"TorrentTransfer": {
"name": "自动转移做种",
"description": "定期转移下载器中的做种任务到另一个下载器。",
"labels": "做种",
"version": "1.3",
"icon": "seed.png",
"author": "jxxghp",
@@ -244,6 +271,7 @@
"RssSubscribe": {
"name": "自定义订阅",
"description": "定时刷新RSS报文识别内容后添加订阅或直接下载。",
"labels": "订阅",
"version": "1.3",
"icon": "rss.png",
"author": "jxxghp",
@@ -255,6 +283,7 @@
"SyncDownloadFiles": {
"name": "下载器文件同步",
"description": "同步下载器的文件信息到数据库,删除文件时联动删除下载任务。",
"labels": "下载管理",
"version": "1.1",
"icon": "Youtube-dl_A.png",
"author": "thsrite",
@@ -263,6 +292,7 @@
"BrushFlow": {
"name": "站点刷流",
"description": "自动托管刷流,将会提高对应站点的访问频率。",
"labels": "刷流",
"version": "2.9",
"icon": "brush.jpg",
"author": "jxxghp,InfinityPacer",
@@ -278,6 +308,7 @@
"DownloadingMsg": {
"name": "下载进度推送",
"description": "定时推送正在下载进度。",
"labels": "消息通知,下载管理",
"version": "1.1",
"icon": "downloadmsg.png",
"author": "thsrite",
@@ -286,6 +317,7 @@
"AutoClean": {
"name": "定时清理媒体库",
"description": "定时清理用户下载的种子、源文件、媒体库文件。",
"labels": "媒体库",
"version": "1.1",
"icon": "clean.png",
"author": "thsrite",
@@ -294,6 +326,7 @@
"InvitesSignin": {
"name": "药丸签到",
"description": "药丸论坛签到。",
"labels": "站点",
"version": "1.4",
"icon": "invites.png",
"author": "thsrite",
@@ -305,6 +338,7 @@
"PersonMeta": {
"name": "演职人员刮削",
"description": "刮削演职人员图片以及中文名称。",
"labels": "媒体库,刮削",
"version": "1.2",
"icon": "actor.png",
"author": "jxxghp",
@@ -313,6 +347,7 @@
"MoviePilotUpdateNotify": {
"name": "MoviePilot更新推送",
"description": "MoviePilot推送release更新通知、自动重启。",
"labels": "消息通知,自动更新",
"version": "1.4",
"icon": "Moviepilot_A.png",
"author": "thsrite",
@@ -325,6 +360,7 @@
"CloudDiskDel": {
"name": "云盘文件删除",
"description": "媒体库删除strm文件后同步删除云盘资源。",
"labels": "媒体库",
"version": "1.3",
"icon": "clouddisk.png",
"author": "thsrite",
@@ -333,6 +369,7 @@
"BarkMsg": {
"name": "Bark消息推送",
"description": "支持使用Bark发送消息通知。",
"labels": "消息通知",
"version": "1.1",
"icon": "Bark_A.png",
"author": "jxxghp",
@@ -341,6 +378,7 @@
"IyuuMsg": {
"name": "IYUU消息推送",
"description": "支持使用IYUU发送消息通知。",
"labels": "消息通知,IYUU",
"version": "1.2",
"icon": "Iyuu_A.png",
"author": "jxxghp",
@@ -349,6 +387,7 @@
"PushDeerMsg": {
"name": "PushDeer消息推送",
"description": "支持使用PushDeer发送消息通知。",
"labels": "消息通知",
"version": "1.1",
"icon": "pushdeer.png",
"author": "jxxghp",
@@ -357,6 +396,7 @@
"ConfigCenter": {
"name": "配置中心",
"description": "快速调整部分系统设定。",
"labels": "系统设置",
"version": "2.4",
"icon": "setting.png",
"author": "jxxghp",
@@ -365,6 +405,7 @@
"WorkWechatMsg": {
"name": "企微机器人消息推送",
"description": "支持使用企业微信群聊机器人发送消息通知。",
"labels": "消息通知",
"version": "1.0",
"icon": "Wecom_A.png",
"author": "叮叮当",
@@ -373,6 +414,7 @@
"EpisodeGroupMeta": {
"name": "TMDB剧集组刮削",
"description": "从TMDB剧集组刮削季集的实际顺序。",
"labels": "刮削",
"version": "1.1",
"icon": "Element_A.png",
"author": "叮叮当",
@@ -381,6 +423,7 @@
"CustomIndexer": {
"name": "自定义索引站点",
"description": "修改或扩展内建索引器支持的站点。",
"labels": "站点",
"version": "1.0",
"icon": "spider.png",
"author": "jxxghp",
@@ -389,6 +432,7 @@
"FFmpegThumb": {
"name": "FFmpeg缩略图",
"description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图",
"labels": "刮削",
"version": "1.2",
"icon": "ffmpeg.png",
"author": "jxxghp",
@@ -397,6 +441,7 @@
"PushPlusMsg": {
"name": "PushPlus消息推送",
"description": "支持使用PushPlus发送消息通知。",
"labels": "消息通知",
"version": "1.0",
"icon": "Pushplus_A.png",
"author": "cheng",
@@ -405,6 +450,7 @@
"DownloadSiteTag": {
"name": "下载任务分类与标签",
"description": "自动给下载任务分类与打站点标签、剧集名称标签",
"labels": "下载管理",
"version": "2.1",
"icon": "Youtube-dl_B.png",
"author": "叮叮当",
@@ -416,6 +462,7 @@
"RemoveLink": {
"name": "清理硬链接",
"description": "监控目录内文件被删除时,同步删除监控目录内所有和它硬链接的文件",
"labels": "文件整理",
"version": "1.9",
"icon": "Ombi_A.png",
"author": "DzAvril",
@@ -430,6 +477,7 @@
"LinkMonitor": {
"name": "实时硬链接",
"description": "监控目录文件变化,实时硬链接。",
"labels": "文件整理",
"version": "1.6",
"icon": "Linkace_C.png",
"author": "jxxghp",
@@ -441,6 +489,7 @@
"CategoryEditor": {
"name": "二级分类策略",
"description": "编辑下载目录和媒体库目录的二级分类规则。",
"labels": "文件整理",
"version": "1.1",
"icon": "Bookstack_A.png",
"author": "jxxghp",
@@ -449,6 +498,7 @@
"RemoteIdentifiers": {
"name": "共享识别词",
"description": "从Github、Etherpad等远程文件中获取共享识别词并应用。",
"labels": "识别",
"version": "2.2",
"icon": "words.png",
"author": "honue",
@@ -457,6 +507,7 @@
"NeoDBSync": {
"name": "NeoDB 想看",
"description": "同步 NeoDB 想看条目,自动添加订阅。",
"labels": "订阅",
"version": "1.1",
"icon": "NeoDB.jpeg",
"author": "hcplantern",
@@ -468,6 +519,7 @@
"PlayletCategory": {
"name": "短剧自动分类",
"description": "网络短剧自动整理到独立的分类目录。",
"labels": "文件整理",
"version": "1.4",
"icon": "Amule_A.png",
"author": "jxxghp",
@@ -476,6 +528,7 @@
"DiagParamAdjust": {
"name": "诊断参数调整",
"description": "Emby专用插件|暂时性解决emby字幕偏移问题需要emby安装Diagnostics插件。",
"labels": "Emby",
"version": "1.3",
"icon": "Gatus_A.png",
"author": "jeblove",
@@ -484,14 +537,19 @@
"QbCommand": {
"name": "QB远程操作",
"description": "通过定时任务或交互命令远程操作QB暂停/开始/限速等。",
"version": "1.3",
"labels": "下载管理,Qbittorrent",
"version": "1.4",
"icon": "Qbittorrent_A.png",
"author": "DzAvril",
"level": 1
"level": 1,
"history": {
"v1.4": "可选某些站点不再做种(暂停做种后不会被恢复)"
}
},
"TrCommand": {
"name": "TR远程操作",
"description": "通过定时任务或交互命令远程操作TR暂停/开始/限速等。",
"labels": "下载管理,Transmission",
"version": "1.1",
"icon": "Transmission_A.png",
"author": "Hoey",
@@ -500,6 +558,7 @@
"IpDetect": {
"name": "本地IP检测",
"description": "如果QB、TR等服务在本地部署当本地IP改变时自动修改其Server IP。",
"labels": "系统设置",
"version": "1.1",
"icon": "ipAddress.png",
"author": "DzAvril",
@@ -508,6 +567,7 @@
"TrackerEditor": {
"name": "Tracker替换",
"description": "批量替换种子tracker支持周期性巡检如为TR仅支持4.0以上版本)。",
"labels": "做种",
"version": "1.5",
"icon": "trackereditor_A.png",
"author": "honue",
@@ -516,6 +576,7 @@
"ContractCheck": {
"name": "契约检查",
"description": "定时检查保种契约达成情况。",
"labels": "做种",
"version": "1.1",
"icon": "contract.png",
"author": "DzAvril",
@@ -524,6 +585,7 @@
"DownloaderHelper": {
"name": "下载器助手",
"description": "自动做种、站点标签、自动删种。",
"labels": "下载管理",
"version": "1.6",
"icon": "DownloaderHelper.png",
"author": "hotlcc",
@@ -535,6 +597,7 @@
"FeiShuMsg": {
"name": "飞书机器人消息通知",
"description": "支持使用飞书群聊机器人发送消息通知。",
"labels": "消息通知",
"version": "1.0",
"icon": "FeiShu_A.png",
"author": "InfinityPacer",
@@ -543,6 +606,7 @@
"IyuuAuth": {
"name": "IYUU站点绑定",
"description": "为IYUU账号绑定认证站点以便用于用户认证和辅种。",
"labels": "IYUU",
"version": "1.1",
"icon": "Iyuu_A.png",
"author": "jxxghp",
@@ -554,6 +618,7 @@
"NtfyMsg": {
"name": "ntfy消息推送",
"description": "支持使用ntfy发送消息通知。",
"labels": "消息通知",
"version": "1.0",
"icon": "Ntfy_A.png",
"author": "lethargicScribe",
@@ -562,9 +627,19 @@
"PluginAutoUpgrade": {
"name": "插件自动升级",
"description": "定时检测、升级插件。",
"labels": "自动更新",
"version": "1.4",
"icon": "PluginAutoUpgrade.png",
"author": "hotlcc",
"level": 1
},
"MergeSiteSwitch": {
"name": "聚合站点开关",
"description": "统一管理所有与站点相关的开关。",
"labels": "系统设置",
"version": "1.0",
"icon": "world.png",
"author": "hotlcc",
"level": 2
}
}

View File

@@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase):
# 插件图标
plugin_icon = "IYUU.png"
# 插件版本
plugin_version = "1.3"
plugin_version = "1.5"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -760,7 +760,7 @@ class IYUUAutoSeed(_PluginBase):
site_domain = StringUtils.get_url_domain(site_url)
# 站点信息
site_info = self.sites.get_indexer(site_domain)
if not site_info:
if not site_info or not site_info.get('url'):
logger.debug(f"没有维护种子对应的站点:{site_url}")
return False
if self._sites and site_info.get('id') not in self._sites:
@@ -892,7 +892,36 @@ class IYUUAutoSeed(_PluginBase):
拼装种子下载链接
"""
def __is_special_site(url):
def __is_mteam(url: str):
"""
判断是否为mteam站点
"""
return True if "m-team." in url else False
def __get_mteam_enclosure(tid: str):
"""
获取mteam种子下载链接
"""
_apikey = self.systemconfig.get(f"site.m-team.apikey")
if not _apikey:
logger.error("m-team站点的apikey未配置")
return None
with RequestUtils(
headers={
'Content-Type': 'application/json',
'User-Agent': f'{site.get("ua")}',
'Accept': 'application/json, text/plain, */*',
'x-api-key': _apikey
}
).post_res(f"{site.get('url')}api/torrent/genDlToken", params={
'id': tid
}) as res:
if not res:
logger.warn(f"m-team 获取种子下载链接失败:{tid}")
return None
return res.json().get("data")
def __is_special_site(url: str):
"""
判断是否为特殊站点
"""
@@ -910,7 +939,10 @@ class IYUUAutoSeed(_PluginBase):
return False
try:
if __is_special_site(site.get('url')):
if __is_mteam(site.get('url')):
# 调用mteam接口获取下载链接
return __get_mteam_enclosure(seed.get("torrent_id"))
elif __is_special_site(site.get('url')):
# 从详情页面获取下载链接
return self.__get_torrent_url_from_page(seed=seed, site=site)
else:
@@ -940,7 +972,7 @@ class IYUUAutoSeed(_PluginBase):
flags=re.IGNORECASE)
return f"{site.get('url')}{download_url}"
except Exception as e:
logger.warn(f"站点 {site.get('name')} Url转换失败{str(e)},尝试通过详情页面获取种子下载链接 ...")
logger.warn(f"{site.get('name')} Url转换失败{str(e)}site_url={site.get('url')}base_url={base_url}, seed={seed}")
return self.__get_torrent_url_from_page(seed=seed, site=site)
def __get_torrent_url_from_page(self, seed: dict, site: dict):

View File

@@ -0,0 +1,912 @@
import inspect
import os
from typing import Any, List, Dict, Tuple
from app.core.event import eventmanager, Event
from app.db.models.site import Site
from app.db.site_oper import SiteOper
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import SystemConfigKey, EventType
class MergeSiteSwitch(_PluginBase):
# 插件名称
plugin_name = "聚合站点开关"
# 插件描述
plugin_desc = "统一管理所有与站点相关的开关。"
# 插件图标
plugin_icon = "world.png"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "hotlcc"
# 作者主页
author_url = "https://github.com/hotlcc"
# 插件配置项ID前缀
plugin_config_prefix = "com.hotlcc.mergesiteswitch."
# 加载顺序
plugin_order = 66
# 可使用的用户级别
auth_level = 2
# 依赖组件
# 站点操作
__site_oper: SiteOper = SiteOper()
# 系统配置操作
__system_config_oper: SystemConfigOper = SystemConfigOper()
# 其它插件ID
# 站点自动签到
__plugin_id_auto_signin: str = 'AutoSignIn'
# 站点数据统计
__plugin_id_site_statistic: str = 'SiteStatistic'
# IYUU自动辅种
__plugin_id_iyuu_auto_seed: str = 'IYUUAutoSeed'
# 站点刷流
__plugin_id_brush_flow: str = 'BrushFlow'
# 配置相关
# 插件缺省配置
__config_default: Dict[str, Any] = {}
# 插件用户配置
__config: Dict[str, Any] = {}
def init_plugin(self, config: dict = None):
"""
初始化插件
"""
# 加载配置
self.__config = config
# 当页面通过调用接口保存配置时保存其它各项配置
if self.__check_stack_is_save_config_request():
self.__set_config(config=config)
def get_state(self) -> bool:
"""
获取插件状态
"""
return self.__check_any_follow_enable_sites()
@staticmethod
def get_command() -> List[Dict[str, Any]]:
"""
定义远程控制命令
:return: 命令关键字、事件、描述、附带数据
"""
pass
def get_api(self) -> List[Dict[str, Any]]:
"""
获取插件API
"""
pass
def get_service(self) -> List[Dict[str, Any]]:
"""
注册插件公共服务
"""
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
"""
# 站点选项
site_options = self.__get_site_options()
# 已安装的插件IDs
installed_plugin_ids = self.__get_installed_plugin_ids()
# 建议的配置
config_suggest = {}
# 表单内容
form_content = [{
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 12, 'xl': 12, 'lg': 12, 'md': 12, 'sm': 12, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'enable_sites',
'label': '启用的站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '对应功能【站点管理 / 添加编辑站点 / 启用】'
}
}]
}]
}, {
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'search_sites',
'label': '设定 / 搜索 / 搜索站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '只有选中的站点才会在搜索中使用。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'search_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
}, {
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'rss_sites',
'label': '设定 / 订阅 / 订阅站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '只有选中的站点才会在订阅中使用。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'rss_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
}]
# 站点自动签到
if self.__plugin_id_auto_signin in installed_plugin_ids:
form_content.append({
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'signin_sites',
'label': '插件 / 站点自动签到 / 签到站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '只有选中的站点才会在签到中使用。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'signin_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'login_sites',
'label': '插件 / 站点自动签到 / 登录站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '只有选中的站点才会在登录中使用。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'login_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
})
# 站点数据统计
if self.__plugin_id_site_statistic in installed_plugin_ids:
form_content.append({
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'statistic_sites',
'label': '插件 / 站点数据统计 / 统计站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '缺省时默认全部站点。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'statistic_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
})
# IYUU自动辅种
if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids:
form_content.append({
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'iyuu_seed_sites',
'label': '插件 / IYUU自动辅种 / 辅种站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '缺省时默认全部站点。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'iyuu_seed_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
})
# 站点刷流
if self.__plugin_id_brush_flow in installed_plugin_ids:
form_content.append({
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12
},
'content': [{
'component': 'VSelect',
'props': {
'model': 'brush_flow_sites',
'label': '插件 / 站点刷流 / 刷流站点',
'multiple': True,
'chips': True,
'items': site_options,
'hint': '只有选中的站点才会在刷流中使用。'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': 'brush_flow_follow_enable_sites',
'label': '跟随启用的站点',
'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。'
}
}]
}]
})
# 提示信息
form_content.append({
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 12, 'xl': 12, 'lg': 12, 'md': 12, 'sm': 12, 'xs': 12
},
'content': [{
'component': 'VAlert',
'props': {
'type': 'warning',
'variant': 'tonal'
},
'text': '点击保存后会立即生效,各项站点开关配置即会更新!'
}]
}]
})
# 表单
form = [{
'component': 'VForm',
'content': form_content
}]
# 重载配置
self.__get_config()
return form, config_suggest
def get_page(self) -> List[dict]:
pass
def stop_service(self):
"""
退出插件
"""
pass
@classmethod
def __check_stack_exist_function(cls, package_name: str, function_name: str) -> bool:
"""
判断当前调用栈是否存在指定的函数
"""
if not package_name or not function_name:
return False
for stack in inspect.stack():
if stack.function != 'set_plugin_config':
continue
package_path = package_name.replace('.', os.sep)
if stack.filename.endswith(f"{package_path}.py") or stack.filename.endswith(f"{package_path}{os.sep}__init__.py"):
return True
return False
@classmethod
def __check_stack_is_save_config_request(cls) -> bool:
"""
判断当前调用栈是否来源于“插件配置保存”接口
"""
return cls.__check_stack_exist_function('app.api.endpoints.plugin', 'set_plugin_config')
def __get_config_value(self, config_key: str, use_default: bool = True) -> Any:
"""
获取插件配置值
:param config_key: 配置键
:param use_default: 是否使用缺省值
:return: 配置值
"""
if not config_key:
return None
config = self.__config if self.__config else {}
config_value = config.get(config_key)
if config_value is None and use_default:
config_default = self.__config_default if self.__config_default else {}
config_value = config_default.get(config_key)
return config_value
def __check_follow_enable_sites(self, config_key: str, plugin_id: str = None, installed_plugin_ids: List[str] = None) -> bool:
"""
判断某个跟随按钮是否打开
:param plugin_id: 传插件ID时还要同时根据插件是否安装进行判断
"""
if not self.__get_config_value(config_key=config_key):
return False
if not plugin_id:
return True
if not installed_plugin_ids:
installed_plugin_ids = self.__get_installed_plugin_ids()
return plugin_id in installed_plugin_ids
def __check_search_follow_enable_sites(self) -> bool:
"""
判断搜索站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='search_follow_enable_sites')
def __check_rss_follow_enable_sites(self) -> bool:
"""
判断订阅站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='rss_follow_enable_sites')
def __check_signin_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool:
"""
判断签到站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='signin_follow_enable_sites', plugin_id=self.__plugin_id_auto_signin, installed_plugin_ids=installed_plugin_ids)
def __check_login_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool:
"""
判断登录站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='login_follow_enable_sites', plugin_id=self.__plugin_id_auto_signin, installed_plugin_ids=installed_plugin_ids)
def __check_statistic_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool:
"""
判断统计站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='statistic_follow_enable_sites', plugin_id=self.__plugin_id_site_statistic, installed_plugin_ids=installed_plugin_ids)
def __check_iyuu_seed_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool:
"""
判断iyuu辅种站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='iyuu_seed_follow_enable_sites', plugin_id=self.__plugin_id_iyuu_auto_seed, installed_plugin_ids=installed_plugin_ids)
def __check_brush_flow_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool:
"""
判断刷流站点的跟随按钮是否打开
"""
return self.__check_follow_enable_sites(config_key='brush_flow_follow_enable_sites', plugin_id=self.__plugin_id_brush_flow, installed_plugin_ids=installed_plugin_ids)
def __check_any_follow_enable_sites(self) -> bool:
"""
判断是否开启任意跟随按钮
"""
# 已安装的插件IDs
installed_plugin_ids = self.__get_installed_plugin_ids()
return True if self.__check_search_follow_enable_sites() \
or self.__check_rss_follow_enable_sites() \
or self.__check_signin_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \
or self.__check_login_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \
or self.__check_statistic_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \
or self.__check_iyuu_seed_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \
or self.__check_brush_flow_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) else False
def __get_site_options(self) -> List[Dict[str, Any]]:
"""
获取站点选项
"""
sites = self.__site_oper.list_order_by_pri()
if not sites:
return []
return [{
'title': site.name,
'value': site.id
} for site in sites if site]
def __get_installed_plugin_ids(self):
"""
获取已安装的插件IDs
"""
installed_plugin_ids = self.__system_config_oper.get(SystemConfigKey.UserInstalledPlugins)
return installed_plugin_ids if installed_plugin_ids else []
def __get_config(self):
"""
获取配置,包含聚合外部配置
"""
config = self.get_config()
if not config:
config = {}
config.update({
'enable_sites': self.__get_enable_site_ids(),
'search_sites': self.__get_search_site_ids(),
'rss_sites': self.__get_rss_site_ids(),
})
# 已安装的插件IDs
installed_plugin_ids = self.__get_installed_plugin_ids()
if self.__plugin_id_auto_signin in installed_plugin_ids:
config.update({
'signin_sites': self.__get_signin_site_ids(),
'login_sites': self.__get_login_site_ids(),
})
if self.__plugin_id_site_statistic in installed_plugin_ids:
config.update({
'statistic_sites': self.__get_statistic_site_ids(),
})
if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids:
config.update({
'iyuu_seed_sites': self.__get_iyuu_seed_site_ids(),
})
if self.__plugin_id_brush_flow in installed_plugin_ids:
config.update({
'brush_flow_sites': self.__get_brush_flow_site_ids(),
})
self.update_config(config=config)
return config
def __pre_config_follow_enable_sites(self, config: dict) -> dict:
"""
处理跟随站点
"""
if not config:
config = {}
enable_sites = config.get('enable_sites') or []
if config.get('search_follow_enable_sites'):
config.update({"search_sites": enable_sites.copy()})
if config.get('rss_follow_enable_sites'):
config.update({"rss_sites": enable_sites.copy()})
if config.get('signin_follow_enable_sites'):
config.update({"signin_sites": enable_sites.copy()})
if config.get('login_follow_enable_sites'):
config.update({"login_sites": enable_sites.copy()})
if config.get('statistic_follow_enable_sites'):
config.update({"statistic_sites": enable_sites.copy()})
if config.get('iyuu_seed_follow_enable_sites'):
config.update({"iyuu_seed_sites": enable_sites.copy()})
if config.get('brush_flow_follow_enable_sites'):
config.update({"brush_flow_sites": enable_sites.copy()})
return config
def __pre_config(self, config: dict) -> dict:
"""
预处理配置
"""
config = self.__pre_config_follow_enable_sites(config=config)
logger.debug(f"配置预处理完成: {config}")
return config
def __set_config(self, config: dict):
"""
保存配置,包含保存外部配置到各自表
"""
# 预处理配置
config = self.__pre_config(config=config)
# 更新各项配置
self.update_config(config=config)
logger.info("插件配置更新完成")
self.__set_enable_site_ids(config.get('enable_sites'))
self.__set_search_site_ids(config.get('search_sites'))
self.__set_rss_site_ids(config.get('rss_sites'))
# 已安装的插件IDs
installed_plugin_ids = self.__get_installed_plugin_ids()
if self.__plugin_id_auto_signin in installed_plugin_ids:
self.__set_signin_site_ids(config.get('signin_sites'))
self.__set_login_site_ids(config.get('login_sites'))
if self.__plugin_id_site_statistic in installed_plugin_ids:
self.__set_statistic_site_ids(config.get('statistic_sites'))
if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids:
self.__set_iyuu_seed_site_ids(config.get('iyuu_seed_sites'))
if self.__plugin_id_brush_flow in installed_plugin_ids:
self.__set_brush_flow_site_ids(config.get('brush_flow_sites'))
return config
def __get_enable_site_ids(self) -> List[int]:
"""
获取启用的站点IDs
"""
sites = self.__site_oper.list_order_by_pri()
if not sites:
return []
return [site.id for site in sites if site and site.is_active]
def __set_enable_site_ids(self, site_ids: List[int]):
"""
设置启用的站点IDs
"""
sites = self.__site_oper.list_order_by_pri()
if not sites:
return
for site in sites:
if not site_ids or site.id not in site_ids:
if site.is_active:
self.__site_oper.update(site.id, {'is_active': False})
else:
if not site.is_active:
self.__site_oper.update(site.id, {'is_active': True})
logger.info("启用的站点配置完成")
def __get_search_site_ids(self) -> List[int]:
"""
获取搜索站点IDs
"""
sites = self.__system_config_oper.get(SystemConfigKey.IndexerSites)
return sites if sites else []
def __set_search_site_ids(self, site_ids: List[int]):
"""
设置搜索站点IDs
"""
self.__system_config_oper.set(SystemConfigKey.IndexerSites, site_ids)
logger.info("搜索站点配置完成")
def __get_rss_site_ids(self) -> List[int]:
"""
获取订阅站点IDs
"""
sites = self.__system_config_oper.get(SystemConfigKey.RssSites)
return sites if sites else []
def __set_rss_site_ids(self, site_ids: List[int]):
"""
设置订阅站点IDs
"""
self.__system_config_oper.set(SystemConfigKey.RssSites, site_ids)
logger.info("订阅站点配置完成")
def __get_plugin_config_value(self, plugin_id: str, config_key: str) -> Any:
"""
获取插件配置值
"""
if not plugin_id or not config_key:
return None
config = self.get_config(plugin_id)
if not config:
return None
return config.get(config_key)
def __set_plugin_config_value(self, plugin_id: str, config_key: str, config_value: Any) -> Any:
"""
设置插件配置值
"""
if not plugin_id or not config_key:
return
config = self.get_config(plugin_id)
if not config:
config = {}
config.update({config_key: config_value})
self.update_config(plugin_id=plugin_id, config=config)
def __get_signin_site_ids(self) -> List[int]:
"""
获取签到站点IDs
"""
sites = self.__get_plugin_config_value(self.__plugin_id_auto_signin, 'sign_sites')
return sites if sites else []
def __set_signin_site_ids(self, site_ids: List[int]):
"""
设置签到站点IDs
"""
self.__set_plugin_config_value(self.__plugin_id_auto_signin, 'sign_sites', site_ids)
logger.info("签到站点配置完成")
def __get_login_site_ids(self) -> List[int]:
"""
获取登录站点IDs
"""
sites = self.__get_plugin_config_value(self.__plugin_id_auto_signin, 'login_sites')
return sites if sites else []
def __set_login_site_ids(self, site_ids: List[int]):
"""
设置登录站点IDs
"""
self.__set_plugin_config_value(self.__plugin_id_auto_signin, 'login_sites', site_ids)
logger.info("登录站点配置完成")
def __get_statistic_site_ids(self) -> List[int]:
"""
获取统计站点IDs
"""
sites = self.__get_plugin_config_value(self.__plugin_id_site_statistic, 'statistic_sites')
return sites if sites else []
def __set_statistic_site_ids(self, site_ids: List[int]):
"""
设置统计站点IDs
"""
self.__set_plugin_config_value(self.__plugin_id_site_statistic, 'statistic_sites', site_ids)
logger.info("统计站点配置完成")
def __get_iyuu_seed_site_ids(self) -> List[int]:
"""
获取iyuu自动辅种站点IDs
"""
sites = self.__get_plugin_config_value(self.__plugin_id_iyuu_auto_seed, 'sites')
return sites if sites else []
def __set_iyuu_seed_site_ids(self, site_ids: List[int]):
"""
设置iyuu自动辅种站点IDs
"""
self.__set_plugin_config_value(self.__plugin_id_iyuu_auto_seed, 'sites', site_ids)
logger.info("IYUU辅种站点配置完成")
def __get_brush_flow_site_ids(self) -> List[int]:
"""
获取刷流站点IDs
"""
sites = self.__get_plugin_config_value(self.__plugin_id_brush_flow, 'brushsites')
return sites if sites else []
def __set_brush_flow_site_ids(self, site_ids: List[int]):
"""
设置刷流站点IDs
"""
self.__set_plugin_config_value(self.__plugin_id_brush_flow, 'brushsites', site_ids)
logger.info("刷流站点配置完成")
def __update_search_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_search_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_search_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_search_site_ids(site_ids=site_ids)
def __update_rss_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_rss_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_rss_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_rss_site_ids(site_ids=site_ids)
def __update_signin_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_signin_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_signin_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_signin_site_ids(site_ids=site_ids)
def __update_login_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_login_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_login_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_login_site_ids(site_ids=site_ids)
def __update_statistic_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_statistic_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_statistic_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_statistic_site_ids(site_ids=site_ids)
def __update_iyuu_seed_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_iyuu_seed_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_iyuu_seed_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_iyuu_seed_site_ids(site_ids=site_ids)
def __update_brush_flow_site_ids_by_site(self, site_id: int, site_status: bool):
if site_id == None:
return
site_ids = self.__get_brush_flow_site_ids() or []
if site_id not in site_ids and site_status:
site_ids.append(site_id)
self.__set_brush_flow_site_ids(site_ids=site_ids)
elif site_id in site_ids and not site_status:
site_ids.remove(site_id)
self.__set_brush_flow_site_ids(site_ids=site_ids)
def __update_site_ids_for_site_event(self, site_id: int, site_status: bool):
"""
针对站点事件更新各项配置
"""
if site_id == None:
return
if self.__check_search_follow_enable_sites():
self.__update_search_site_ids_by_site(site_id=site_id, site_status=site_status)
if self.__check_rss_follow_enable_sites():
self.__update_rss_site_ids_by_site(site_id=site_id, site_status=site_status)
# 已安装的插件IDs
installed_plugin_ids = self.__get_installed_plugin_ids()
if self.__check_signin_follow_enable_sites(installed_plugin_ids=installed_plugin_ids):
self.__update_signin_site_ids_by_site(site_id=site_id, site_status=site_status)
if self.__check_login_follow_enable_sites(installed_plugin_ids=installed_plugin_ids):
self.__update_login_site_ids_by_site(site_id=site_id, site_status=site_status)
if self.__check_statistic_follow_enable_sites(installed_plugin_ids=installed_plugin_ids):
self.__update_statistic_site_ids_by_site(site_id=site_id, site_status=site_status)
if self.__check_iyuu_seed_follow_enable_sites(installed_plugin_ids=installed_plugin_ids):
self.__update_iyuu_seed_site_ids_by_site(site_id=site_id, site_status=site_status)
if self.__check_brush_flow_follow_enable_sites(installed_plugin_ids=installed_plugin_ids):
self.__update_brush_flow_site_ids_by_site(site_id=site_id, site_status=site_status)
@eventmanager.register(EventType.SiteUpdated)
def listen_site_updated_event(self, event: Event = None):
"""
监听站点更新事件
"""
logger.info('监听到站点更新事件')
if not event or not event.event_data:
logger.warn('事件信息无效,忽略事件')
return
domain = event.event_data.get("domain")
if not domain:
logger.warn('事件信息无效,忽略事件')
return
if not self.__check_any_follow_enable_sites():
logger.warn('未打开任一【跟随启用的站点】开关,忽略事件')
return
site = self.__site_oper.get_by_domain(domain=domain)
if not site:
logger.warn(f'目标站点不存在,忽略事件: domain = {domain}')
return
self.__update_site_ids_for_site_event(site_id=site.id, site_status=site.is_active)
logger.info('站点更新事件监听任务执行完成')
@eventmanager.register(EventType.SiteDeleted)
def listen_site_deleted_event(self, event: Event = None):
"""
监听站点删除事件
"""
logger.info('监听到站点删除事件')
if not event or not event.event_data:
logger.warn('事件信息无效,忽略事件')
return
site_id = event.event_data.get("site_id")
if site_id == None:
logger.warn('事件信息无效,忽略事件')
return
if not self.__check_any_follow_enable_sites():
logger.warn('未打开任一【跟随启用的站点】开关,忽略事件')
return
self.__update_site_ids_for_site_event(site_id=site_id, site_status=False)
logger.info('站点删除事件监听任务执行完成')

View File

@@ -1,5 +1,7 @@
from typing import List, Tuple, Dict, Any
from enum import Enum
from urllib.parse import urlparse
import urllib
from app.log import logger
from app.modules.qbittorrent import Qbittorrent
from app.plugins import _PluginBase
@@ -7,6 +9,13 @@ from app.schemas import NotificationType
from app.schemas.types import EventType
from apscheduler.triggers.cron import CronTrigger
from app.core.event import eventmanager, Event
from apscheduler.schedulers.background import BackgroundScheduler
from app.core.config import settings
from app.helper.sites import SitesHelper
from app.db.site_oper import SiteOper
from app.utils.string import StringUtils
from datetime import datetime, timedelta
import pytz
import time
@@ -18,7 +27,7 @@ class QbCommand(_PluginBase):
# 插件图标
plugin_icon = "Qbittorrent_A.png"
# 插件版本
plugin_version = "1.3"
plugin_version = "1.4"
# 插件作者
plugin_author = "DzAvril"
# 作者主页
@@ -31,6 +40,8 @@ class QbCommand(_PluginBase):
auth_level = 1
# 私有属性
_sites = None
_siteoper = None
_qb = None
_enabled: bool = False
_notify: bool = False
@@ -45,8 +56,14 @@ class QbCommand(_PluginBase):
_enable_upload_limit = False
_download_limit = 0
_enable_download_limit = False
_op_site_ids = []
_op_sites = []
_multi_level_root_domain = ["edu.cn", "com.cn", "net.cn", "org.cn"]
_scheduler = None
def init_plugin(self, config: dict = None):
self._sites = SitesHelper()
self._siteoper = SiteOper()
# 停止现有任务
self.stop_service()
# 读取配置
@@ -65,14 +82,33 @@ class QbCommand(_PluginBase):
self._enable_download_limit = config.get("enable_download_limit")
self._enable_upload_limit = config.get("enable_upload_limit")
self._qb = Qbittorrent()
self._op_site_ids = config.get("op_site_ids") or []
# 查询所有站点
all_sites = [site for site in self._sites.get_indexers() if not site.get("public")] + self.__custom_sites()
# 过滤掉没有选中的站点
self._op_sites = [site for site in all_sites if site.get("id") in self._op_site_ids]
if self._only_pause_once or self._only_resume_once:
if self._only_pause_once and self._only_resume_once:
logger.warning("只能选择一个: 立即暂停或立即开始所有任务")
elif self._only_pause_once:
self.pause_torrent()
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"立即运行一次暂停所有任务")
self._scheduler.add_job(
self.pause_torrent,
"date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
)
elif self._only_resume_once:
self.resume_torrent()
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"立即运行一次开始所有任务")
self._scheduler.add_job(
self.resume_torrent,
"date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
)
self._only_resume_once = False
self._only_pause_once = False
@@ -84,16 +120,56 @@ class QbCommand(_PluginBase):
"notify": self._notify,
"pause_cron": self._pause_cron,
"resume_cron": self._resume_cron,
"op_site_ids": self._op_site_ids,
}
)
if self._only_pause_upload or self._only_pause_download or self._only_pause_checking:
# 启动任务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
self._scheduler.start()
if (
self._only_pause_upload
or self._only_pause_download
or self._only_pause_checking
):
if self._only_pause_upload:
self.pause_torrent(self.TorrentType.UPLOADING)
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"立即运行一次暂停所有上传任务")
self._scheduler.add_job(
self.pause_torrent,
"date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
kwargs={
'type': self.TorrentType.UPLOADING
}
)
if self._only_pause_download:
self.pause_torrent(self.TorrentType.DOWNLOADING)
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"立即运行一次暂停所有下载任务")
self._scheduler.add_job(
self.pause_torrent,
"date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
kwargs={
'type': self.TorrentType.DOWNLOADING
}
)
if self._only_pause_checking:
self.pause_torrent(self.TorrentType.CHECKING)
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
logger.info(f"立即运行一次暂停所有检查任务")
self._scheduler.add_job(
self.pause_torrent,
"date",
run_date=datetime.now(tz=pytz.timezone(settings.TZ))
+ timedelta(seconds=3),
kwargs={
'type': self.TorrentType.CHECKING
}
)
self._only_pause_upload = False
self._only_pause_download = False
@@ -107,9 +183,15 @@ class QbCommand(_PluginBase):
"notify": self._notify,
"pause_cron": self._pause_cron,
"resume_cron": self._resume_cron,
"op_site_ids": self._op_site_ids,
}
)
# 启动任务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
self._scheduler.start()
self.set_limit(self._upload_limit, self._download_limit)
def get_state(self) -> bool:
@@ -186,6 +268,13 @@ class QbCommand(_PluginBase):
},
]
def __custom_sites(self) -> List[Any]:
custom_sites = []
custom_sites_config = self.get_config("CustomSites")
if custom_sites_config and custom_sites_config.get("enabled"):
custom_sites = custom_sites_config.get("sites")
return custom_sites
def get_api(self) -> List[Dict[str, Any]]:
pass
@@ -427,6 +516,7 @@ class QbCommand(_PluginBase):
return
all_torrents = self.get_all_torrents()
all_torrents = self.filter_torrents(all_torrents)
hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = (
self.get_torrents_status(all_torrents)
)
@@ -489,6 +579,41 @@ class QbCommand(_PluginBase):
f"错误数量: {len(hash_error)}\n",
)
def filter_torrents(self, all_torrents):
"""
过滤掉不参与保种的种子
"""
if len(self._op_sites) == 0:
return all_torrents
urls = [site.get("url") for site in self._op_sites]
op_sites_main_domains = []
for url in urls:
domain = StringUtils.get_url_netloc(url)
main_domain = self.get_main_domain(domain[1])
op_sites_main_domains.append(main_domain)
torrents = []
for torrent in all_torrents:
if torrent.get("state") == "pausedUP":
tracker_url = self.get_torrent_tracker(torrent)
if not tracker_url:
logger.info(f"获取种子 {torrent.name} Tracker失败不过滤该种子")
torrents.append(torrent)
_, tracker_domain = StringUtils.get_url_netloc(tracker_url)
if not tracker_domain:
logger.info(f"获取种子 {torrent.name} Tracker失败不过滤该种子")
torrents.append(torrent)
tracker_main_domain = self.get_main_domain(domain=tracker_domain)
if tracker_main_domain in op_sites_main_domains:
logger.info(
f"种子 {torrent.name} 属于站点{tracker_main_domain},不执行操作"
)
continue
torrents.append(torrent)
return torrents
@eventmanager.register(EventType.PluginAction)
def handle_qb_status(self, event: Event):
if not self._enabled:
@@ -508,14 +633,13 @@ class QbCommand(_PluginBase):
self.get_torrents_status(all_torrents)
)
logger.info(
f"QB开始任务启动 \n"
f"QB任务状态 \n"
f"种子总数: {len(all_torrents)} \n"
f"做种数量: {len(hash_uploading)}\n"
f"下载数量: {len(hash_downloading)}\n"
f"检查数量: {len(hash_checking)}\n"
f"暂停数量: {len(hash_paused)}\n"
f"错误数量: {len(hash_error)}\n"
f"开始操作中请稍等...\n",
)
if self._notify:
self.post_message(
@@ -648,7 +772,67 @@ class QbCommand(_PluginBase):
text=f"设置QB限速失败",
)
def get_torrent_tracker(self, torrent):
"""
qb解析 tracker
:return: tracker url
"""
if not torrent:
return None
tracker = torrent.get("tracker")
if tracker and len(tracker) > 0:
return tracker
magnet_uri = torrent.get("magnet_uri")
if not magnet_uri or len(magnet_uri) <= 0:
return None
magnet_uri_obj = urlparse(magnet_uri)
query = urllib.parse.parse_qs(magnet_uri_obj.query)
tr = query["tr"]
if not tr or len(tr) <= 0:
return None
return tr[0]
def get_main_domain(self, domain):
"""
获取域名的主域名
:param domain: 原域名
:return: 主域名
"""
if not domain:
return None
domain_arr = domain.split(".")
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]}"
def match_multi_level_root_domain(self, domain):
"""
匹配多级根域名
: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 get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
customSites = self.__custom_sites()
site_options = [
{"title": site.name, "value": site.id}
for site in self._siteoper.list_order_by_pri()
] + [
{"title": site.get("name"), "value": site.get("id")} for site in customSites
]
return [
{
"component": "VForm",
@@ -849,6 +1033,27 @@ class QbCommand(_PluginBase):
},
],
},
{
"component": "VRow",
"content": [
{
"component": "VCol",
"props": {"cols": 12},
"content": [
{
"component": "VSelect",
"props": {
"chips": True,
"multiple": True,
"model": "op_site_ids",
"label": "停止保种站点(暂停保种后不会被恢复)",
"items": site_options,
},
}
],
}
],
},
{
"component": "VRow",
"content": [
@@ -884,22 +1089,6 @@ class QbCommand(_PluginBase):
}
],
},
{
"component": "VCol",
"props": {
"cols": 12,
},
"content": [
{
"component": "VAlert",
"props": {
"type": "info",
"variant": "tonal",
"text": "PT精神重在分享请勿恶意限速因此导致账号被封禁作者概不负责",
},
}
],
},
],
},
],
@@ -916,10 +1105,21 @@ class QbCommand(_PluginBase):
"download_limit": 0,
"enable_upload_limit": False,
"enable_download_limit": False,
"op_site_ids": [],
}
def get_page(self) -> List[dict]:
pass
def stop_service(self):
pass
"""
退出插件
"""
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))