Compare commits

...

33 Commits

Author SHA1 Message Date
jxxghp
88b29169fc Merge pull request #890 from wumode/clashruleprovider 2025-09-02 18:23:59 +08:00
wumode
2c9e108ac4 fix(ClashRuleProvider): 保持键名一致性 2025-09-02 13:24:24 +08:00
wumode
73b2d778a0 fix(ClashRuleProvider): 配置模板保存问题 2025-09-02 12:39:13 +08:00
jxxghp
bf67d6e567 Merge pull request #889 from wumode/clashruleprovider 2025-09-01 20:38:22 +08:00
wumode
5e9da0802d update(ClashRuleProvider): 优化性能 2025-09-01 20:38:03 +08:00
wumode
2811021996 update(ClashRuleProvider): 优化 UI 2025-09-01 20:21:09 +08:00
jxxghp
8c0a05b2de Merge pull request #888 from wumode/lexiannot 2025-08-29 18:45:54 +08:00
wumode
bb070bf83e 使用字典键直接访问 token 2025-08-29 18:41:27 +08:00
wumode
21aec36ea5 update(LexiAnnot): 避免spaCy模型常驻内存 2025-08-29 15:36:48 +08:00
jxxghp
6019cf92ac fix BugReporter 2025-08-28 08:21:15 +08:00
jxxghp
42d5dd1e89 fix BugReporter 2025-08-27 17:43:58 +08:00
jxxghp
0b3313e078 update PersonMeta 2025-08-27 16:07:01 +08:00
jxxghp
5684ba056a update package.v2.json 2025-08-27 09:59:39 +08:00
jxxghp
44af7dbb78 add BugReporter 2025-08-27 09:53:15 +08:00
jxxghp
2102a03740 Merge pull request #885 from wumode/clashruleprovider 2025-08-24 18:46:30 +08:00
wumode
0a9cadf7ab update(ClashRuleProvider): 通过emoji识别国家 2025-08-24 18:06:43 +08:00
jxxghp
279efe8000 Merge pull request #883 from wumode/lexiannot 2025-08-23 17:18:18 +08:00
wumode
fd92e58f81 update(ImdbSource) 修复错误 2025-08-23 16:58:00 +08:00
wumode
fe93e46e02 update(ImdbSource) 修改UA 2025-08-23 00:01:13 +08:00
wumode
cbf541992f update(LexiAnnot): 添加任务页面 2025-08-22 17:03:07 +08:00
jxxghp
8e1d336250 add 统一缓存使用说明 2025-08-21 16:06:23 +08:00
jxxghp
12e0e2b9f5 Merge pull request #881 from wumode/imdbsource 2025-08-20 00:34:31 +08:00
wumode
ac914f70f3 update: ImdbSource&ToBypassTrackers 2025-08-20 00:10:09 +08:00
jxxghp
a07b8a4f4a Merge pull request #878 from wumode/lexiannot 2025-08-17 20:13:53 +08:00
wumode
6960b3f7aa update(LexiAnnot): 支持考试词汇标注 2025-08-17 19:50:30 +08:00
jxxghp
fe83ff1be8 Merge pull request #876 from liuhangbin/multiclass 2025-08-14 19:40:50 +08:00
Hangbin Liu
6357dc8e4a plugins.v2: 添加多级分类插件
目前MP默认只支持二级分类,但是部分用户有多级目录的需求,比如增加按照年代
或者评分度分类。因此增加一个支持多级分类的插件, 目前仅支持电影多级分类。

Signed-off-by: Hangbin Liu <liuhangbin@gmail.com>
2025-08-14 19:16:45 +08:00
jxxghp
f1d94d0aa3 Merge pull request #875 from yelantf/main 2025-08-12 12:08:56 +08:00
夜阑听风
53dd3bc796 Update dingdingmsg in package.json 2025-08-12 10:59:53 +08:00
夜阑听风
a9d528fc05 Update dingdingmsg version 2025-08-12 10:58:25 +08:00
夜阑听风
0388c437b1 update dingdingmsg to support breakline 2025-08-12 10:56:24 +08:00
jxxghp
ac4b53e745 AutoSignIn v2.7 2025-08-12 08:25:09 +08:00
jxxghp
53297fccaf 更新 release.yml 2025-08-10 13:48:02 +08:00
69 changed files with 8964 additions and 5112 deletions

View File

@@ -90,15 +90,18 @@ jobs:
rm -f "$asset"
(cd "$(dirname "$plugin_dir")" && zip -r "$GITHUB_WORKSPACE/$asset" "$(basename "$plugin_dir")" -x "*/__pycache__/*" -x "*.pyc") >/dev/null
# If same tag exists, delete release and remote tag first
# If same tag exists, delete release and both remote/local tag first
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag exists, deleting..."
gh release delete "$tag" -y
git push origin :refs/tags/"$tag" || true
fi
# Ensure no stale local tag remains
git tag -d "$tag" >/dev/null 2>&1 || true
echo "Creating release $tag"
gh release create "$tag" "$asset" --title "$tag" --notes "Automated release of $plugin_id $plugin_version" --latest
gh release create "$tag" "$asset" --title "$tag" --notes "Automated release of $plugin_id $plugin_version" --latest --target "$GITHUB_SHA"
echo "$tag" >> processed_tags.txt
done

185
README.md
View File

@@ -23,6 +23,7 @@ MoviePilot官方插件市场https://github.com/jxxghp/MoviePilot-Plugins
- [12. 如何通过插件扩展支持的存储类型?](#12-如何通过插件扩展支持的存储类型)
- [13. 如何将插件功能集成到工作流?](#13-如何将插件功能集成到工作流)
- [14. 如何在插件中通过消息持续与用户交互?](#14-如何在插件中通过消息持续与用户交互)
- [15. 如何在插件中使用系统级统一缓存?](#15-如何在插件中使用系统级统一缓存)
- [版本发布](#版本发布)
- [1. 如何发布插件版本?](#1-如何发布插件版本)
- [2. 如何开发V2版本的插件以及实现插件多版本兼容](#2-如何开发v2版本的插件以及实现插件多版本兼容)
@@ -1167,6 +1168,190 @@ def get_actions(self) -> List[Dict[str, Any]]:
- 建议在交互中保存用户状态数据,以支持复杂的多步骤操作
- 可以结合插件数据存储功能保存用户的交互历史和偏好设置
### 15. 如何在插件中使用系统级统一缓存?
**(仅支持 `v2.7.4+` 版本)**
- MoviePilot提供了统一的缓存系统支持内存缓存、文件系统缓存和Redis缓存自动管理当有Redis时优先使用Redis否则使用内存或文件系统。插件可以通过系统提供的缓存接口实现高效的缓存管理无需关心系统设置。
- 1. 使用缓存装饰器:
```python
from app.core.cache import cached
class MyPlugin(_PluginBase):
@cached(region="my_plugin", ttl=3600)
def get_data(self, key: str):
"""
使用缓存装饰器缓存结果1小时
"""
# 复杂的计算或网络请求
return expensive_operation(key)
@cached(region="my_plugin_async", ttl=1800, skip_none=True)
async def get_async_data(self, key: str):
"""
异步函数缓存跳过None值
"""
return await async_expensive_operation(key)
```
- 2. 使用TTLCache类
```python
from app.core.cache import TTLCache
class MyPlugin(_PluginBase):
def __init__(self):
super().__init__()
# 创建缓存实例最大128项TTL 30分钟
self.cache = TTLCache(region="my_plugin", maxsize=128, ttl=1800)
def process_data(self, key: str):
# 检查缓存
if key in self.cache:
return self.cache[key]
# 计算并缓存结果
result = expensive_operation(key)
self.cache[key] = result
return result
def clear_cache(self):
"""
清理插件缓存
"""
self.cache.clear()
```
- 3. 使用文件缓存后端(适用于大文件缓存):
```python
from app.core.cache import FileCache, AsyncFileCache
from pathlib import Path
class MyPlugin(_PluginBase):
def __init__(self):
super().__init__()
# 获取文件缓存后端支持Redis和文件系统
self.file_cache = FileCache(
base=Path("/tmp/my_plugin_cache"),
ttl=86400 # 24小时
)
def cache_large_file(self, key: str, data: bytes):
"""
缓存大文件数据
"""
self.file_cache.set(key, data, region="large_files")
def get_cached_file(self, key: str) -> Optional[bytes]:
"""
获取缓存的文件数据
"""
return self.file_cache.get(key, region="large_files")
async def async_cache_operations(self):
"""
异步文件缓存操作
"""
async_cache = AsyncFileCache(
base=Path("/tmp/my_plugin_async_cache"),
ttl=3600
)
# 异步设置缓存
await async_cache.set("async_key", b"async_data", region="async_files")
# 异步获取缓存
data = await async_cache.get("async_key", region="async_files")
await async_cache.close()
```
- 4. 直接使用缓存后端(高级用法):
```python
from app.core.cache import Cache
class MyPlugin(_PluginBase):
def __init__(self):
super().__init__()
# 直接获取缓存后端实例系统自动选择Redis或内存缓存
self.cache_backend = Cache(maxsize=256, ttl=3600)
def custom_cache_operation(self, key: str, value: Any):
"""
自定义缓存操作
"""
# 设置缓存
self.cache_backend.set(key, value, region="custom_region")
# 检查缓存是否存在
if self.cache_backend.exists(key, region="custom_region"):
# 获取缓存
cached_value = self.cache_backend.get(key, region="custom_region")
return cached_value
return None
def iterate_cache_items(self):
"""
遍历缓存项
"""
for key, value in self.cache_backend.items(region="custom_region"):
print(f"缓存键: {key}, 值: {value}")
def cleanup(self):
"""
清理缓存
"""
self.cache_backend.clear(region="custom_region")
self.cache_backend.close()
```
- 5. 缓存装饰器参数说明:
```python
@cached(
region="my_plugin", # 缓存区域,用于隔离不同插件的缓存
maxsize=512, # 最大缓存条目数(仅内存缓存有效)
ttl=1800, # 缓存存活时间(秒)
skip_none=True, # 是否跳过None值缓存
skip_empty=False # 是否跳过空值缓存(空列表、空字典等)
)
def my_function(self, param):
pass
```
- 6. 缓存管理功能:
```python
class MyPlugin(_PluginBase):
@cached(region="my_plugin")
def cached_function(self, param):
return expensive_operation(param)
def clear_my_cache(self):
"""
清理指定区域的缓存
"""
self.cached_function.cache_clear()
def get_cache_info(self):
"""
获取缓存信息
"""
cache_region = self.cached_function.cache_region
return f"缓存区域: {cache_region}"
```
- 7. 缓存后端自动选择:
- 系统会根据配置自动选择缓存后端:
- `CACHE_BACKEND_TYPE=redis`使用Redis作为缓存后端
- `CACHE_BACKEND_TYPE=memory`使用内存缓存cachetools
- 插件代码无需修改,系统会自动处理缓存后端的切换
- 8. 最佳实践:
- 为每个插件使用独立的缓存区域region避免缓存键冲突
- 合理设置TTL避免缓存过期时间过长导致数据过期
- 对于频繁访问的数据使用较长的TTL对于实时性要求高的数据使用较短的TTL
- 使用`skip_none=True`避免缓存无意义的None值
- 大文件或二进制数据建议使用文件缓存后端
- 在插件卸载时清理相关缓存,避免内存泄漏
## 版本发布

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -943,11 +943,14 @@
"name": "钉钉机器人",
"description": "支持使用钉钉机器人发送消息通知。",
"labels": "消息通知,钉钉机器人",
"version": "1.12",
"version": "1.13",
"icon": "Dingding_A.png",
"author": "nnlegenda",
"level": 1,
"v2": true
"v2": true,
"history": {
"v1.13": "优化钉钉消息换行"
}
},
"DynamicWeChat": {
"name": "动态企微可信IP",

View File

@@ -42,12 +42,13 @@
"name": "站点自动签到",
"description": "自动模拟登录、签到站点。",
"labels": "站点",
"version": "2.6",
"version": "2.7",
"icon": "signin.png",
"author": "thsrite",
"level": 2,
"release": true,
"history": {
"v2.7": "站点请求使用站点设置的超时时间",
"v2.6": "感谢madrays佬提供的UI!",
"v2.5.4": "增加保号风险提示",
"v2.5.3": "优化执行周期输入需要MoviePilot v2.2.1+",
@@ -182,11 +183,12 @@
"name": "演职人员刮削",
"description": "刮削演职人员图片以及中文名称。",
"labels": "媒体库,刮削",
"version": "2.2",
"version": "2.2.1",
"icon": "actor.png",
"author": "jxxghp",
"level": 1,
"history": {
"v2.2.1": "优化错误数据兼容处理",
"v2.2": "修改使用自定义图片域名时无法下载图片的问题",
"v2.1": "优化执行周期输入需要MoviePilot v2.2.1+",
"v2.0": "兼容MoviePilot V2 版本",
@@ -350,6 +352,18 @@
"v2.0": "适配新的目录结构变化,短剧分类名称调整为配置目录路径,升级后需要重新调整设置后才能使用。"
}
},
"MultiClass": {
"name": "视频多级分类",
"description": "支持视频多级分类",
"labels": "文件整理",
"version": "0.1",
"icon": "Calibreweb_B.png",
"author": "liuhangbin",
"level": 1,
"history": {
"v0.1": "视频多级分类插件, 目前仅支持电影按评分,年代,系列分类。"
}
},
"MoviePilotUpdateNotify": {
"name": "MoviePilot更新推送",
"description": "MoviePilot推送release更新通知、自动重启。",
@@ -419,11 +433,12 @@
"name": "绕过Trackers",
"description": "提供tracker服务器IP地址列表帮助IPv6连接绕过OpenClash。",
"labels": "工具",
"version": "1.4.2",
"version": "1.4.3",
"icon": "Clash_A.png",
"author": "wumode",
"level": 2,
"history": {
"v1.4.3": "修复 bug",
"v1.4.2": "修复插件动作",
"v1.4.1": "修复通知类型错误",
"v1.4": "异步查询DNS",
@@ -437,11 +452,13 @@
"name": "IMDb源",
"description": "让探索推荐和媒体识别支持IMDb数据源。",
"labels": "探索",
"version": "1.5.6",
"version": "1.5.8",
"icon": "IMDb_IOS-OSX_App.png",
"author": "wumode",
"level": 1,
"history": {
"v1.5.8": "修改UA",
"v1.5.7": "改进异常处理",
"v1.5.6": "固定仪表盘组件海报比例; 修复 bug",
"v1.5.5": "修复初始化错误",
"v1.5.4": "改进媒体识别",
@@ -467,12 +484,15 @@
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "1.3.2",
"version": "1.4.1",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"release": true,
"history": {
"v1.4.1": "修复配置模板保存错误, 请重新配置Clash模板",
"v1.4.0": "优化 UI; 支持连接多个 Clash Dashboards",
"v1.3.3": "通过emoji识别国家; 按国家分组节点; mrs格式支持",
"v1.3.2": "注册插件动作",
"v1.3.1": "支持配置 Hosts",
"v1.2.8": "改进导入界面",
@@ -495,11 +515,14 @@
"name": "美剧生词标注",
"description": "根据CEFR等级为英语影视剧标注高级词汇。",
"labels": "英语",
"version": "1.0.1",
"version": "1.1.2",
"icon": "LexiAnnot.png",
"author": "wumode",
"level": 1,
"history": {
"v1.1.2": "使用子进程避免 spaCy 模型常驻内存",
"v1.1.1": "添加任务页面; 改进 spaCy 模型加载逻辑",
"v1.1.0": "支持考试词汇标注; 优化分词处理; 修复错误",
"v1.0.1": "合并连字符词; 避免ARM平台依赖问题",
"v1.0": "新增LexiAnnot"
}
@@ -516,5 +539,18 @@
"v1.0.0": "首个版本新增MeoW消息通知",
"v1.0.1": "优化代码,修复运行一次按钮没办法自动关闭的问题"
}
},
"BugReporter": {
"name": "Bug反馈",
"description": "自动上报异常,协助开发者发现和解决问题。",
"labels": "开发",
"version": "1.2",
"icon": "Alist_encrypt_A.png",
"author": "jxxghp",
"level": 1,
"history": {
"v1.2": "优化上报信息量",
"v1.1": "加强脱敏处理"
}
}
}

View File

@@ -7,10 +7,6 @@ from typing import Any, List, Dict, Tuple, Optional
from urllib.parse import urljoin
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from ruamel.yaml import CommentedMap
from app import schemas
from app.core.config import settings
from app.core.event import eventmanager, Event
@@ -26,6 +22,9 @@ from app.utils.http import RequestUtils
from app.utils.site import SiteUtils
from app.utils.string import StringUtils
from app.utils.timer import TimerUtils
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from ruamel.yaml import CommentedMap
class AutoSignIn(_PluginBase):
@@ -36,7 +35,7 @@ class AutoSignIn(_PluginBase):
# 插件图标
plugin_icon = "signin.png"
# 插件版本
plugin_version = "2.6"
plugin_version = "2.7"
# 插件作者
plugin_author = "thsrite"
# 作者主页
@@ -1545,6 +1544,7 @@ class AutoSignIn(_PluginBase):
render = site_info.get("render")
proxies = settings.PROXY if site_info.get("proxy") else None
proxy_server = settings.PROXY_SERVER if site_info.get("proxy") else None
timeout = site_info.get("timeout") or 60
if not site_url or not site_cookie:
logger.warn(f"未配置 {site} 的站点地址或Cookie无法签到")
return False, ""
@@ -1560,7 +1560,8 @@ class AutoSignIn(_PluginBase):
page_source = PlaywrightHelper().get_page_source(url=checkin_url,
cookies=site_cookie,
ua=ua,
proxies=proxy_server)
proxies=proxy_server,
timeout=timeout)
if not SiteUtils.is_logged_in(page_source):
if under_challenge(page_source):
return False, f"无法通过Cloudflare"
@@ -1574,13 +1575,15 @@ class AutoSignIn(_PluginBase):
else:
res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).get_res(url=checkin_url)
if not res and site_url != checkin_url:
logger.info(f"开始站点模拟登录:{site},地址:{site_url}...")
res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).get_res(url=site_url)
# 判断登录状态
if res and res.status_code in [200, 500, 403]:
@@ -1647,6 +1650,7 @@ class AutoSignIn(_PluginBase):
render = site_info.get("render")
proxies = settings.PROXY if site_info.get("proxy") else None
proxy_server = settings.PROXY_SERVER if site_info.get("proxy") else None
timeout = site_info.get("timeout") or 60
if not site_url or not site_cookie:
logger.warn(f"未配置 {site} 的站点地址或Cookie无法签到")
return False, ""
@@ -1659,7 +1663,8 @@ class AutoSignIn(_PluginBase):
page_source = PlaywrightHelper().get_page_source(url=site_url,
cookies=site_cookie,
ua=ua,
proxies=proxy_server)
proxies=proxy_server,
timeout=timeout)
if not SiteUtils.is_logged_in(page_source):
if under_challenge(page_source):
return False, f"无法通过Cloudflare"
@@ -1669,7 +1674,8 @@ class AutoSignIn(_PluginBase):
else:
res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).get_res(url=site_url)
# 判断登录状态
if res and res.status_code in [200, 500, 403]:

View File

@@ -2,13 +2,12 @@ import random
import re
from typing import Tuple
from lxml import etree
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
from lxml import etree
class Pt52(_ISiteSigninHandler):
@@ -46,14 +45,16 @@ class Pt52(_ISiteSigninHandler):
ua = site_info.get("ua")
render = site_info.get("render")
proxy = site_info.get("proxy")
timeout = site_info.get("timeout")
# 判断今日是否已签到
html_text = self.get_page_source(url='https://52pt.site/bakatest.php',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'
@@ -97,14 +98,16 @@ class Pt52(_ISiteSigninHandler):
site_cookie=site_cookie,
ua=ua,
proxy=proxy,
site=site)
site=site,
timeout=timeout)
def __signin(self, questionid: str,
choice: list,
site: str,
site_cookie: str,
ua: str,
proxy: bool) -> Tuple[bool, str]:
proxy: bool,
timeout: int) -> Tuple[bool, str]:
"""
签到请求
questionid: 450
@@ -124,7 +127,8 @@ class Pt52(_ISiteSigninHandler):
sign_res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=settings.PROXY if proxy else None
proxies=settings.PROXY if proxy else None,
timeout=timeout
).post_res(url='https://52pt.site/bakatest.php', data=data)
if not sign_res or sign_res.status_code != 200:
logger.error(f"{site} 签到失败,签到接口请求失败")

View File

@@ -42,7 +42,8 @@ class _ISiteSigninHandler(metaclass=ABCMeta):
pass
@staticmethod
def get_page_source(url: str, cookie: str, ua: str, proxy: bool, render: bool, token: str = None) -> str:
def get_page_source(url: str, cookie: str, ua: str, proxy: bool, render: bool,
token: str = None, timeout: int = None) -> str:
"""
获取页面源码
:param url: Url地址
@@ -51,13 +52,15 @@ class _ISiteSigninHandler(metaclass=ABCMeta):
:param proxy: 是否使用代理
:param render: 是否渲染
:param token: JWT Token
:param timeout: 请求超时时间,单位秒
:return: 页面源码,错误信息
"""
if render:
return PlaywrightHelper().get_page_source(url=url,
cookies=cookie,
ua=ua,
proxies=settings.PROXY_SERVER if proxy else None)
proxies=settings.PROXY_SERVER if proxy else None,
timeout=timeout or 60)
else:
if token:
headers = {
@@ -70,7 +73,8 @@ class _ISiteSigninHandler(metaclass=ABCMeta):
"Cookie": cookie
}
res = RequestUtils(headers=headers,
proxies=settings.PROXY if proxy else None).get_res(url=url)
proxies=settings.PROXY if proxy else None,
timeout=timeout or 20).get_res(url=url)
if res is not None:
# 使用chardet检测字符编码
raw_data = res.content

View File

@@ -37,6 +37,7 @@ class BTSchool(_ISiteSigninHandler):
ua = site_info.get("ua")
render = site_info.get("render")
proxy = site_info.get("proxy")
timeout = site_info.get("timeout")
logger.info(f"{site} 开始签到")
# 判断今日是否已签到
@@ -44,7 +45,8 @@ class BTSchool(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
@@ -63,7 +65,8 @@ class BTSchool(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,签到接口请求失败")

View File

@@ -47,13 +47,15 @@ class CHDBits(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 判断今日是否已签到
html_text = self.get_page_source(url='https://ptchdbits.co/bakatest.php',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")

View File

@@ -37,21 +37,24 @@ class HaiDan(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 签到
# 签到页会重定向到index.php由于302重定向特性导致index.php没有携带cookie
self.get_page_source(url='https://www.haidan.video/signin.php',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render,
timeout=timeout)
# 重新携带cookie获取index.php查看签到结果
html_text = self.get_page_source(url='https://www.haidan.video/index.php',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -40,13 +40,15 @@ class Hares(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 获取页面html
html_text = self.get_page_source(url='https://club.hares.top',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 模拟访问失败,请检查站点连通性")
@@ -66,7 +68,8 @@ class Hares(_ISiteSigninHandler):
}
sign_res = RequestUtils(cookies=site_cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
proxies=settings.PROXY if proxy else None,
timeout=timeout
).get_res(url="https://club.hares.top/attendance.php?action=sign")
if not sign_res or sign_res.status_code != 200:
logger.error(f"{site} 签到失败,签到接口请求失败")

View File

@@ -40,6 +40,7 @@ class HDArea(_ISiteSigninHandler):
site_cookie = site_info.get("cookie")
ua = site_info.get("ua")
proxies = settings.PROXY if site_info.get("proxy") else None
timeout = site_info.get("timeout")
# 获取页面html
data = {
@@ -47,7 +48,8 @@ class HDArea(_ISiteSigninHandler):
}
html_res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).post_res(url="https://hdarea.club/sign_in.php", data=data)
if not html_res or html_res.status_code != 200:
logger.error(f"{site} 签到失败,请检查站点连通性")

View File

@@ -40,6 +40,7 @@ class HDChina(_ISiteSigninHandler):
site_cookie = site_info.get("cookie")
ua = site_info.get("ua")
proxies = settings.PROXY if site_info.get("proxy") else None
timeout = site_info.get("timeout")
# 尝试解决瓷器cookie每天签到后过期,只保留hdchina=部分
cookie = ""
@@ -59,7 +60,8 @@ class HDChina(_ISiteSigninHandler):
# 获取页面html
html_res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).get_res(url="https://hdchina.org/index.php")
if not html_res or html_res.status_code != 200:
logger.error(f"{site} 签到失败,请检查站点连通性")
@@ -99,7 +101,8 @@ class HDChina(_ISiteSigninHandler):
}
sign_res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).post_res(url="https://hdchina.org/plugin_sign-in.php?cmd=signin", data=data)
if not sign_res or sign_res.status_code != 200:
logger.error(f"{site} 签到失败,签到接口请求失败")

View File

@@ -39,13 +39,15 @@ class HDCity(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 获取页面html
html_text = self.get_page_source(url='https://hdcity.city/sign',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -43,13 +43,15 @@ class HDSky(_ISiteSigninHandler):
proxy = site_info.get("proxy")
render = site_info.get("render")
referer = site_info.get("url")
timeout = site_info.get("timeout")
# 判断今日是否已签到
html_text = self.get_page_source(url='https://hdsky.me',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'
@@ -73,7 +75,8 @@ class HDSky(_ISiteSigninHandler):
content_type='application/x-www-form-urlencoded; charset=UTF-8',
referer="https://hdsky.me/index.php",
accept_type="*/*",
proxies=settings.PROXY if proxy else None
proxies=settings.PROXY if proxy else None,
timeout=timeout
).post_res(url='https://hdsky.me/image_code_ajax.php',
data={'action': 'new'})
if image_res and image_res.status_code == 200:

View File

@@ -41,13 +41,15 @@ class HDUpt(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 获取页面html
html_text = self.get_page_source(url='https://pt.hdupt.com',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'
@@ -67,7 +69,8 @@ class HDUpt(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -1,5 +1,4 @@
from typing import Tuple
from urllib.parse import urljoin
from ruamel.yaml import CommentedMap
@@ -38,10 +37,11 @@ class MTorrent(_ISiteSigninHandler):
"Authorization": site_info.get("token")
}
url = site_info.get('url')
timeout = site_info.get("timeout")
domain = StringUtils.get_url_domain(url)
# 更新最后访问时间
res = RequestUtils(headers=headers,
timeout=60,
timeout=timeout,
proxies=settings.PROXY if site_info.get("proxy") else None,
referer=f"{url}index"
).post_res(url=f"https://api.{domain}/api/member/updateLastBrowse")

View File

@@ -40,6 +40,7 @@ class NexusHD(_ISiteSigninHandler):
site_cookie = site_info.get("cookie")
ua = site_info.get("ua")
proxies = settings.PROXY if site_info.get("proxy") else None
timeout = site_info.get("timeout")
# 获取页面html
data = {
@@ -48,7 +49,8 @@ class NexusHD(_ISiteSigninHandler):
}
html_res = RequestUtils(cookies=site_cookie,
ua=ua,
proxies=proxies
proxies=proxies,
timeout=timeout
).post_res(url="https://v6.nexushd.org/signin.php", data=data)
if not html_res or html_res.status_code != 200:
logger.error(f"{site} 签到失败,请检查站点连通性")

View File

@@ -43,13 +43,15 @@ class Opencd(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 判断今日是否已签到
html_text = self.get_page_source(url='https://www.open.cd',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -35,13 +35,15 @@ class PTerClub(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 签到
html_text = self.get_page_source(url='https://pterclub.com/attendance-ajax.php',
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -37,6 +37,7 @@ class PTTime(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 签到
# 签到返回:<html><head></head><body>签到成功</body></html>
@@ -44,7 +45,8 @@ class PTTime(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")

View File

@@ -57,6 +57,7 @@ class Tjupt(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 创建正确答案存储目录
if not os.path.exists(os.path.dirname(self._answer_file)):
@@ -67,7 +68,8 @@ class Tjupt(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
# 获取签到后返回html判断是否签到成功
if not html_text:

View File

@@ -44,13 +44,15 @@ class TTG(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 获取页面html
html_text = self.get_page_source(url="https://totheglory.im",
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -50,6 +50,7 @@ class U2(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
now = datetime.datetime.now()
# 判断当前时间是否小于9点
@@ -62,7 +63,8 @@ class U2(_ISiteSigninHandler):
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 签到失败,请检查站点连通性")
return False, '签到失败,请检查站点连通性'

View File

@@ -37,7 +37,7 @@ class YemaPT(_ISiteSigninHandler):
}
# 获取用户信息,更新最后访问时间
res = (RequestUtils(headers=headers,
timeout=15,
timeout=site_info.get("timeout"),
cookies=site_info.get("cookie"),
proxies=settings.PROXY if site_info.get("proxy") else None,
referer=site_info.get('url')
@@ -64,7 +64,7 @@ class YemaPT(_ISiteSigninHandler):
}
# 获取用户信息,更新最后访问时间
res = (RequestUtils(headers=headers,
timeout=15,
timeout=site_info.get("timeout"),
cookies=site_info.get("cookie"),
proxies=settings.PROXY if site_info.get("proxy") else None,
referer=site_info.get('url')

View File

@@ -38,13 +38,15 @@ class ZhuQue(_ISiteSigninHandler):
ua = site_info.get("ua")
proxy = site_info.get("proxy")
render = site_info.get("render")
timeout = site_info.get("timeout")
# 获取页面html
html_text = self.get_page_source(url="https://zhuque.in",
cookie=site_cookie,
ua=ua,
proxy=proxy,
render=render)
render=render,
timeout=timeout)
if not html_text:
logger.error(f"{site} 模拟登录失败,请检查站点连通性")
return False, '模拟登录失败,请检查站点连通性'
@@ -73,7 +75,8 @@ class ZhuQue(_ISiteSigninHandler):
}
skill_res = RequestUtils(cookies=site_cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
proxies=settings.PROXY if proxy else None,
timeout=timeout
).post_res(url="https://zhuque.in/api/gaming/fireGenshinCharacterMagic", json=data)
if not skill_res or skill_res.status_code != 200:
logger.error(f"模拟登录失败,释放技能失败")

View File

@@ -0,0 +1,262 @@
import re
import socket
import ssl
from typing import Any, Dict
from typing import List, Tuple
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
import sentry_sdk
from app.plugins import _PluginBase
from version import APP_VERSION
class SentrySanitizer:
# 常见敏感字段名(可自行扩展)
SENSITIVE_KEYS = {
"password", "passwd", "pwd",
"secret", "token", "access_token", "refresh_token",
"authorization", "api_key", "apikey",
"cookie", "set-cookie", "passkey",
"key", "credential", "auth", "login", "user", "username",
"email", "phone", "address", "ip", "host", "domain"
}
# 匹配包含敏感关键词的正则
SENSITIVE_PATTERN = re.compile(
"|".join(re.escape(key) for key in SENSITIVE_KEYS), re.IGNORECASE
)
# 网络连接错误类异常(不上报)
NETWORK_ERRORS = {
"ConnectionError", "ConnectionRefusedError", "ConnectionAbortedError",
"ConnectionResetError", "TimeoutError", "socket.timeout", "socket.error",
"ssl.SSLError", "ssl.SSLCertVerificationError", "ssl.SSLWantReadError",
"ssl.SSLWantWriteError", "ssl.SSLZeroReturnError", "ssl.SSLSyscallError",
"urllib.error.URLError", "urllib.error.HTTPError", "requests.exceptions.ConnectionError",
"requests.exceptions.Timeout", "requests.exceptions.ConnectTimeout",
"requests.exceptions.ReadTimeout", "requests.exceptions.SSLError",
"aiohttp.ClientConnectionError", "aiohttp.ClientTimeout", "aiohttp.ServerTimeoutError",
"aiohttp.ServerDisconnectedError", "aiohttp.ClientOSError"
}
# 网络连接错误关键词
NETWORK_ERROR_KEYWORDS = [
"connection", "timeout", "network", "dns", "ssl", "certificate",
"refused", "reset", "aborted", "unreachable", "no route to host",
"name or service not known", "temporary failure", "network is unreachable"
]
@classmethod
def scrub_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
"""
递归清洗字典中的敏感信息
"""
if not isinstance(data, dict):
return data
sanitized = {}
for key, value in data.items():
if isinstance(value, dict):
sanitized[key] = cls.scrub_dict(value)
elif isinstance(value, list):
sanitized[key] = [cls.scrub_dict(v) if isinstance(v, dict) else v for v in value]
else:
if cls.SENSITIVE_PATTERN.search(str(key)):
sanitized[key] = "[Filtered]"
else:
sanitized[key] = value
return sanitized
@classmethod
def scrub_url(cls, url: str) -> str:
"""
清理 URL 中的敏感 query 参数
"""
try:
parsed = urlparse(url)
query = parse_qs(parsed.query, keep_blank_values=True)
for key in query:
if cls.SENSITIVE_PATTERN.search(key):
query[key] = ["[Filtered]"]
new_query = urlencode(query, doseq=True)
return urlunparse(parsed._replace(query=new_query))
except Exception as err:
print(str(err))
return url
@classmethod
def is_network_error(cls, event) -> bool:
"""
判断是否为网络连接错误类异常
"""
# 检查异常类型
if "exception" in event:
for exc in event["exception"].get("values", []):
if "type" in exc:
exc_type = exc["type"]
if exc_type in cls.NETWORK_ERRORS:
return True
# 检查异常消息是否包含网络错误关键词
if "value" in exc:
exc_value = exc["value"].lower()
for keyword in cls.NETWORK_ERROR_KEYWORDS:
if keyword in exc_value:
return True
# 检查日志消息
if "message" in event:
message = event["message"].lower()
for keyword in cls.NETWORK_ERROR_KEYWORDS:
if keyword in message:
return True
return False
@classmethod
def before_send(cls, event, hint):
"""
在发送到 Sentry 之前脱敏和过滤
"""
# 如果是网络连接错误,直接返回 None 不上报
if cls.is_network_error(event):
return None
# 处理 request 数据
request = event.get("request", {})
if "url" in request:
request["url"] = cls.scrub_url(request["url"])
if "headers" in request:
request["headers"] = cls.scrub_dict(request["headers"])
if "data" in request:
request["data"] = cls.scrub_dict(request["data"])
if "cookies" in request:
request["cookies"] = cls.scrub_dict(request["cookies"])
# 处理 user 数据
if "user" in event:
event["user"] = cls.scrub_dict(event["user"])
# 处理 extra 数据
if "extra" in event:
event["extra"] = cls.scrub_dict(event["extra"])
# 处理异常信息(避免敏感数据出现在 message 中)
if "exception" in event:
for exc in event["exception"].get("values", []):
if "value" in exc and cls.SENSITIVE_PATTERN.search(exc["value"]):
exc["value"] = "[Filtered Exception Message]"
# 清理异常堆栈中的敏感信息
if "stacktrace" in exc and "frames" in exc["stacktrace"]:
for frame in exc["stacktrace"]["frames"]:
if "vars" in frame:
frame["vars"] = cls.scrub_dict(frame["vars"])
if "context_line" in frame and cls.SENSITIVE_PATTERN.search(frame["context_line"]):
frame["context_line"] = "[Filtered]"
# 清理消息中的敏感信息
if "message" in event and cls.SENSITIVE_PATTERN.search(event["message"]):
event["message"] = "[Filtered Message]"
return event
class BugReporter(_PluginBase):
# 插件名称
plugin_name = "Bug反馈"
# 插件描述
plugin_desc = "自动上报异常,协助开发者发现和解决问题。"
# 插件图标
plugin_icon = "Alist_encrypt_A.png"
# 插件版本
plugin_version = "1.2"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
author_url = "https://github.com/jxxghp"
# 插件配置项ID前缀
plugin_config_prefix = "bugreporter_"
# 加载顺序
plugin_order = 99
# 可使用的用户级别
auth_level = 1
_enable: bool = False
def init_plugin(self, config: dict = None):
self._enable = config.get("enable")
if self._enable:
sentry_sdk.init("https://88da01ad33b4423cb0380620de53efa8@glitchtip.movie-pilot.org/1",
before_send=SentrySanitizer.before_send,
release=APP_VERSION,
send_default_pii=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]]:
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'enable',
'label': '启用插件',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'warning',
'variant': 'tonal',
'text': '注意开启插件即代表你同意将部分异常信息自动发送给开发者以帮助改进软件如果你不希望自动发送任何数据请关闭或卸载此插件仅上报系统异常信息不会包含任何个人隐私信息或敏感数据网络连接错误类异常不会上报异常信息采集为使用开源项目解决方案GlitchTip。',
}
}
]
}
]
}
]
}
], {
"enable": self._enable,
}
def get_page(self) -> List[dict]:
pass
def get_state(self) -> bool:
return self._enable
def stop_service(self):
pass

View File

@@ -0,0 +1 @@
sentry_sdk~=2.35.1

View File

@@ -3,16 +3,22 @@ import copy
import hashlib
import json
import math
import pytz
import re
import time
import urllib
import yaml
from datetime import datetime, timedelta
from typing import Any, Optional, List, Dict, Tuple, Union
from urllib.parse import urlparse
import pytz
import websockets
import yaml
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from fastapi import HTTPException, Request, status, Body, Response
from fastapi.responses import PlainTextResponse
from sse_starlette.sse import EventSourceResponse
from app import schemas
from app.core.config import settings
from app.core.event import eventmanager, Event
@@ -25,10 +31,6 @@ from app.schemas.types import EventType
from app.schemas.types import NotificationType
from app.utils.http import RequestUtils, AsyncRequestUtils
from app.utils.ip import IpUtils
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from fastapi import HTTPException, Request, status, Body, Response
from sse_starlette.sse import EventSourceResponse
class ClashRuleProvider(_PluginBase):
@@ -39,7 +41,7 @@ class ClashRuleProvider(_PluginBase):
# 插件图标
plugin_icon = "Mihomo_Meta_A.png"
# 插件版本
plugin_version = "1.3.2"
plugin_version = "1.4.1"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -58,12 +60,9 @@ class ClashRuleProvider(_PluginBase):
_notify = False
# 订阅链接
_sub_links = []
# Clash 面板 URL
_clash_dashboard_url = None
# Clash 面板密钥
_clash_dashboard_secret = None
# MoviePilot URL
_movie_pilot_url = ''
_movie_pilot_url: str = ''
_cron = ''
_timeout = 10
_retry_times = 3
@@ -71,14 +70,17 @@ class ClashRuleProvider(_PluginBase):
_auto_update_subscriptions = True
_ruleset_prefix: str = '📂<='
_group_by_region: bool = False
_group_by_country: bool = False
_refresh_delay: int = 5
_discard_rules: bool = False
_discard_proxy_groups: bool = False
_enable_acl4ssr: bool = False
_dashboard_components: List[str] = []
_clash_template_yaml: str = ''
_clash_template: str = ''
_hint_geo_dat: bool = False
# Cloudflare 优选 IPs 可通过外部设置
_best_cf_ip: List[str] = []
_apikey: Optional[str] = None
# 插件数据
_top_rules: List[str] = []
@@ -87,21 +89,25 @@ class ClashRuleProvider(_PluginBase):
_extra_rule_providers: Dict[str, Any] = {}
_subscription_info = {}
_ruleset_names: Dict[str, str] = {}
_proxy_groups = []
_proxy_groups: List[Dict[str, Any]] = []
_extra_proxies = []
_acl4ssr_providers: Dict[str, Any] = {}
_acl4ssr_prefix: str = '🗂️=>'
# 保存每个订阅文件的原始内容
_clash_configs: Dict[str, Any] = {}
_hosts: List[Dict[str, Any]] = []
_clash_dashboards: List[Dict[str, str]] = []
_active_dashboard: Optional[int] = None
# protected variables
_clash_rule_parser = None
_ruleset_rule_parser = None
_clash_template: Optional[Dict[str, Any]] = None
_clash_template_dict: Optional[Dict[str, Any]] = None
_scheduler: Optional[BackgroundScheduler] = None
_countries: Optional[List[Dict[str, str]]] = None
_countries: List[Dict[str, str]] = []
_geo_rules: Dict[str, List[str]] = {'geoip': [], 'geosite': []}
_clash_dashboard_url: str = ''
_clash_dashboard_secret: str = ''
def init_plugin(self, config: dict = None):
self.stop_service()
@@ -119,18 +125,22 @@ class ClashRuleProvider(_PluginBase):
if config:
self._enabled = config.get("enabled")
self._proxy = config.get("proxy")
self._notify = config.get("notify"),
self._notify = bool(config.get("notify"))
self._sub_links = config.get("sub_links") or []
self._clash_dashboard_url = config.get("clash_dashboard_url") or ''
if self._clash_dashboard_url and self._clash_dashboard_url[-1] == '/':
self._clash_dashboard_url = self._clash_dashboard_url[:-1]
if not (self._clash_dashboard_url.startswith('http://') or
self._clash_dashboard_url.startswith('https://')):
self._clash_dashboard_url = 'http://' + self._clash_dashboard_url
self._clash_dashboard_secret = config.get("clash_dashboard_secret")
self._movie_pilot_url = config.get("movie_pilot_url")
if self._movie_pilot_url and self._movie_pilot_url[-1] == '/':
self._movie_pilot_url = self._movie_pilot_url[:-1]
clash_dashboards = config.get("clash_dashboards")
if clash_dashboards is None:
clash_dashboards = [{'url': config.get('clash_dashboard_url') or '',
'secret': config.get('clash_dashboard_secret') or ''}]
self._clash_dashboards = []
for clash_dashboard in clash_dashboards:
url = (clash_dashboard.get("url") or '').rstrip('/')
if not (url.startswith('http://') or url.startswith('https://')):
url = 'http://' + url
self._clash_dashboards.append({'url': url, 'secret': clash_dashboard.get('secret') or ''})
self._movie_pilot_url = config.get("movie_pilot_url") or ''
if self._movie_pilot_url:
self._movie_pilot_url = self._movie_pilot_url.rstrip('/')
self._cron = config.get("cron_string") or '30 12 * * *'
self._timeout = config.get("timeout")
self._retry_times = config.get("retry_times") or 3
@@ -139,39 +149,53 @@ class ClashRuleProvider(_PluginBase):
self._acl4ssr_prefix = config.get("acl4ssr_prefix", "🗂️=>")
self._auto_update_subscriptions = config.get("auto_update_subscriptions")
self._group_by_region = config.get("group_by_region")
self._group_by_country = config.get("group_by_country") or False
self._refresh_delay = config.get("refresh_delay") or 5
self._discard_rules = config.get("discard_rules") or False
self._discard_proxy_groups = config.get("discard_proxy_groups") or False
self._enable_acl4ssr = config.get("enable_acl4ssr") or False
self._dashboard_components = config.get("dashboard_components") or []
self._clash_template_yaml = config.get("clash_template") or ''
self._clash_template = config.get("clash_template") or ''
self._hint_geo_dat = config.get("hint_geo_dat", False)
self._best_cf_ip = config.get("best_cf_ip") or []
self._active_dashboard = config.get("active_dashboard")
if self._active_dashboard is None and self._clash_dashboards:
self._active_dashboard = 0
self._apikey = config.get("apikey")
self.__update_config()
self._clash_rule_parser = ClashRuleParser()
self._ruleset_rule_parser = ClashRuleParser()
self._clash_template = {}
self._clash_template_dict = {}
self._countries = []
if self._active_dashboard is not None and self._active_dashboard in range(len(self._clash_dashboards)):
self._clash_dashboard_url = self._clash_dashboards[self._active_dashboard].get("url")
self._clash_dashboard_secret = self._clash_dashboards[self._active_dashboard].get("secret")
if self._enabled:
try:
self._clash_template = yaml.load(self._clash_template_yaml, Loader=yaml.SafeLoader) or {}
if not isinstance(self._clash_template, dict):
self._clash_template = {}
self._clash_template_dict = yaml.load(self._clash_template, Loader=yaml.SafeLoader) or {}
if not isinstance(self._clash_template_dict, dict):
self._clash_template_dict = {}
logger.error(f"Invalid clash template yaml")
# 规范配置模板
self._clash_template['proxies'] = self._clash_template.get('proxies') or []
self._clash_template['proxy-groups'] = self._clash_template.get('proxy-groups') or []
self._clash_template['rule-providers'] = self._clash_template.get('rule-providers') or {}
self._clash_template['rules'] = self._clash_template.get('rules') or []
self._clash_template_dict['proxies'] = self._clash_template_dict.get('proxies') or []
self._clash_template_dict['proxy-groups'] = self._clash_template_dict.get('proxy-groups') or []
self._clash_template_dict['rule-providers'] = self._clash_template_dict.get('rule-providers') or {}
self._clash_template_dict['rules'] = self._clash_template_dict.get('rules') or []
except yaml.YAMLError as exc:
logger.error(f"Error loading clash template yaml: {exc}")
if self._group_by_region:
self._countries = ClashRuleProvider.__load_countries(
f"{settings.ROOT_PATH}/app/plugins/clashruleprovider/countries.json")
settings.ROOT_PATH / 'app' / 'plugins' / 'clashruleprovider' / 'countries.json')
self.__parse_config()
# 清理不存在的 URL
self._subscription_info = {url: self._subscription_info.get(url)
for url in self._sub_links if self._subscription_info.get(url)}
self._subscription_info = {url: self._subscription_info.get(url) or {} for url in self._sub_links}
for _, sub_info in self._subscription_info.items():
if 'enabled' not in sub_info:
sub_info['enabled'] = True
self._clash_configs = {url: self._clash_configs[url] for url in self._sub_links if
self._clash_configs.get(url)}
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
self._scheduler.start()
# 更新订阅
@@ -362,6 +386,7 @@ class ClashRuleProvider(_PluginBase):
"path": "/ruleset",
"endpoint": self.get_ruleset,
"methods": ["GET"],
"allow_anonymous": True if self._apikey else False,
"summary": "获取规则集规则",
"description": "获取规则集规则"
},
@@ -397,10 +422,19 @@ class ClashRuleProvider(_PluginBase):
"summary": "删除一条 Host",
"description": "删除一条 Host"
},
{
"path": "/subscription-info",
"endpoint": self.update_subscription_info,
"methods": ["POST"],
"auth": "bear",
"summary": "更新订阅信息",
"description": "更新订阅信息"
},
{
"path": "/config",
"endpoint": self.get_clash_config,
"methods": ["GET"],
"allow_anonymous": True if self._apikey else False,
"summary": "获取 Clash 配置",
"description": "获取 Clash 配置"
},
@@ -508,6 +542,35 @@ class ClashRuleProvider(_PluginBase):
}]
return []
def __update_config(self):
config = {
'enabled': self._enabled,
'proxy': self._proxy,
'notify': self._notify,
'sub_links': self._sub_links,
'clash_dashboards': self._clash_dashboards,
'movie_pilot_url': self._movie_pilot_url,
'cron': self._cron,
'timeout': self._timeout,
'retry_times': self._retry_times,
'filter_keywords': self._filter_keywords,
'auto_update_subscriptions': self._auto_update_subscriptions,
'ruleset_prefix': self._ruleset_prefix,
'group_by_region': self._group_by_region,
'group_by_country': self._group_by_country,
'refresh_delay': self._refresh_delay,
'discard_rules': self._discard_rules,
'discard_proxy_groups': self._discard_proxy_groups,
'enable_acl4ssr': self._enable_acl4ssr,
'dashboard_components': self._dashboard_components,
'clash_template': self._clash_template,
'hint_geo_dat': self._hint_geo_dat,
'best_cf_ip': self._best_cf_ip,
'active_dashboard': self._active_dashboard,
'apikey': self._apikey
}
self.update_config(config)
def update_best_cf_ip(self, ips: List[str]):
"""
通过深拷贝更新 Cloudflare 优选 IPs
@@ -597,49 +660,81 @@ class ClashRuleProvider(_PluginBase):
async def test_connectivity(self, params: Dict[str, Any]) -> schemas.Response:
if not self._enabled:
return schemas.Response(success=False, message="")
if not params.get('clash_dashboard_url') or not params.get('clash_dashboard_secret') \
or not params.get('sub_links'):
return schemas.Response(success=True, message="missing params")
clash_version_url = f"{params.get('clash_dashboard_url')}/version"
ret = await AsyncRequestUtils(accept_type="application/json",
headers={"authorization": f"Bearer {params.get('clash_dashboard_secret')}"}
).get(clash_version_url)
if ret is None:
return schemas.Response(success=False, message="无法连接到Clash")
if params.get('clash_dashboards') is None or not params.get('sub_links'):
return schemas.Response(success=True, message="Missing params")
tasks = []
for clash_dashboard in params['clash_dashboards']:
url = clash_dashboard.get('url') or ''
secret = clash_dashboard.get('secret') or ''
tasks.append(asyncio.create_task(ClashRuleProvider.async_fetch_clash_version(url, secret)))
results = await asyncio.gather(*tasks)
for i, result in enumerate(results):
if not result:
return schemas.Response(success=False,
message=f"无法连接到 Clash {params['clash_dashboards'][i].get('url')}")
for sub_link in (params.get('sub_links') or []):
ret = await AsyncRequestUtils(accept_type="text/html",
proxies=settings.PROXY if self._proxy else None
).get(sub_link)
if ret is None:
return schemas.Response(success=False, message=f"Unable to fetch {sub_link}")
return schemas.Response(success=False, message=f"无法获取 {sub_link}")
return schemas.Response(success=True, message="测试连接成功")
def get_ruleset(self, name):
if not self._ruleset_names.get(name):
return None
name = self._ruleset_names.get(name)
rules = self.__get_ruleset(name)
@staticmethod
async def async_fetch_clash_version(url: str, secret: str) -> Optional[str]:
url = url.rstrip('/')
clash_version_url = f"{url}/version"
ret = await AsyncRequestUtils(accept_type="application/json",
headers={"authorization": f"Bearer {secret}"},
timeout=5
).get_json(clash_version_url)
return ret
def get_ruleset(self, name: str, apikey: str) -> Response:
_apikey = self._apikey or settings.API_TOKEN
if apikey != _apikey:
raise HTTPException(status_code=403, detail="Invalid API Key")
ruleset_name = self._ruleset_names.get(name)
if ruleset_name is None:
raise HTTPException(status_code=404, detail=f"Ruleset '{name}' not found")
rules = self.__get_ruleset(ruleset_name)
res = yaml.dump({"payload": rules}, allow_unicode=True)
return Response(content=res, media_type="text/yaml")
return PlainTextResponse(content=res, media_type="application/x-yaml")
def get_clash_outbound(self) -> schemas.Response:
outbound = self.clash_outbound()
return schemas.Response(success=True, message="", data={"outbound": outbound})
def get_status(self):
def get_status(self) -> schemas.Response:
first_config = self._clash_configs.get(self._sub_links[0], {}) if self._sub_links else {}
rule_size = len(first_config.get("rules", []))
return {"success": True, "message": "",
"data": {"state": self._enabled,
"ruleset_prefix": self._ruleset_prefix,
"clash": {"rule_size": rule_size},
"best_cf_ip": self._best_cf_ip,
"geoRules": self._geo_rules,
"subscription_info": self._subscription_info,
"sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?"
f"apikey={settings.API_TOKEN}"}}
data = {"state": self._enabled,
"ruleset_prefix": self._ruleset_prefix,
"clash": {"rule_size": rule_size},
"best_cf_ip": self._best_cf_ip,
"geoRules": self._geo_rules,
"subscription_info": self._subscription_info,
"sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?"
f"apikey={self._apikey or settings.API_TOKEN}"}
return schemas.Response(success=True, message="", data=data)
def get_clash_config(self, request: Request):
def update_subscription_info(self, params: Dict[str, Any]) -> schemas.Response:
url = params.get('url')
field = params.get('field')
if 'value' not in params or url not in self._subscription_info or field not in ['name', 'enabled']:
return schemas.Response(success=False, message="Missing params")
value = params.get('value')
self._subscription_info[url][field] = value
self.save_data('subscription_info', self._subscription_info)
return schemas.Response(success=True)
def get_clash_config(self, apikey: str, request: Request):
_apikey = self._apikey or settings.API_TOKEN
if apikey != _apikey:
raise HTTPException(status_code=403, detail="Invalid API Key")
logger.info(f"{request.client.host} 正在获取配置")
config = self.clash_config()
if not config:
@@ -794,7 +889,7 @@ class ClashRuleProvider(_PluginBase):
if not config:
return schemas.Response(success=False, message=f"订阅链接 {url} 更新失败")
self._clash_configs[url] = config
self._subscription_info[url] = info
self._subscription_info[url] = {**info, 'enabled': self._subscription_info.get(url, {}).get('enabled', False)}
self.save_data('clash_configs', self._clash_configs)
self.save_data('subscription_info', self._subscription_info)
return schemas.Response(success=True, message='订阅更新成功')
@@ -816,6 +911,8 @@ class ClashRuleProvider(_PluginBase):
for key, value in new_value.items():
if key == 'name' or value is None:
continue
if key == 'payload' and params.get('type') != 'inline':
continue
if value == '' or value is None:
continue
item[key] = value
@@ -852,7 +949,7 @@ class ClashRuleProvider(_PluginBase):
first_config = self._clash_configs.get(self._sub_links[0], {}) if self._sub_links else {}
proxy_groups = []
sources = ('Manual', 'Template', urlparse(self._sub_links[0]).hostname if self._sub_links else '', 'Region')
groups = (self._proxy_groups, self._clash_template.get('proxy-groups', []),
groups = (self._proxy_groups, self._clash_template_dict.get('proxy-groups', []),
first_config.get('proxy-groups', []), self.proxy_groups_by_region())
for i, group in enumerate(groups):
for proxy_group in group:
@@ -875,7 +972,7 @@ class ClashRuleProvider(_PluginBase):
proxy_copy = copy.deepcopy(proxy)
proxy_copy['source'] = hostname
proxies.append(proxy_copy)
for proxy in self._clash_template.get('proxies', []):
for proxy in self._clash_template_dict.get('proxies', []):
proxy_copy = copy.deepcopy(proxy)
proxy_copy['source'] = 'Template'
proxies.append(proxy_copy)
@@ -999,15 +1096,15 @@ class ClashRuleProvider(_PluginBase):
self.save_data('proxy_groups', self._proxy_groups)
return schemas.Response(success=True, message='')
def clash_outbound(self) -> Optional[List]:
def clash_outbound(self) -> List[Dict[str, Any]]:
first_config = self._clash_configs.get(self._sub_links[0], {}) if self._sub_links else {}
outbound = [{'name': proxy_group.get("name")} for proxy_group in first_config.get("proxy-groups", [])]
outbound.extend([{'name': proxy.get("name")} for proxy in first_config.get("proxies", [])])
if self._clash_template:
if 'proxy-groups' in self._clash_template:
outbound.extend(self._clash_template.get('proxy-groups') or [])
if 'proxies' in self._clash_template:
outbound.extend(self._clash_template.get('proxies') or [])
if self._clash_template_dict:
if 'proxy-groups' in self._clash_template_dict:
outbound.extend(self._clash_template_dict.get('proxy-groups') or [])
if 'proxies' in self._clash_template_dict:
outbound.extend(self._clash_template_dict.get('proxies') or [])
if self._group_by_region:
outbound.extend([{'name': proxy_group.get("name")} for proxy_group in self.proxy_groups_by_region()])
outbound.extend([{'name': proxy.get("name")} for proxy in self._extra_proxies])
@@ -1020,7 +1117,7 @@ class ClashRuleProvider(_PluginBase):
rule_providers = []
provider_sources = (self._extra_rule_providers,
first_config.get('rule-providers', {}),
self._clash_template.get('rule-providers', {}),
self._clash_template_dict.get('rule-providers', {}),
self._acl4ssr_providers)
source_names = ('Manual', hostname, 'Template', 'Auto', 'Acl4ssr')
for i, provider in enumerate(provider_sources):
@@ -1119,8 +1216,6 @@ class ClashRuleProvider(_PluginBase):
def delete_rule_by_priority(self, priority: int, rule_parser: ClashRuleParser
) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
if not isinstance(priority, int):
return None
res = rule_parser.remove_rule_at_priority(priority)
self.__save_data()
return res
@@ -1162,7 +1257,7 @@ class ClashRuleProvider(_PluginBase):
f"总量: {ClashRuleProvider.format_bytes(sub_info.get('total', 0))}\n"
f"过期时间: {ClashRuleProvider.format_expire_time(sub_info.get('expire', 0))}")
else:
info = ""
info = f"节点数量: {sub_info.get('proxy_num', 0)}\n"
message += f"订阅更新成功\n{info}"
else:
message += '订阅更新失败'
@@ -1225,11 +1320,13 @@ class ClashRuleProvider(_PluginBase):
all_proxies = []
res = {}
for index, url in enumerate(self._sub_links):
if not self._subscription_info.get(url, {}).get('enabled'):
continue
config, sub_info = self.__get_subscription(url)
self._subscription_info[url] = sub_info or {}
if not config:
res[url] = False
continue
self._subscription_info[url] = {**sub_info, 'enabled': True}
res[url] = True
self._clash_configs[url] = config
all_proxies.extend(config.get("proxies", []))
@@ -1244,7 +1341,7 @@ class ClashRuleProvider(_PluginBase):
logger.info(f"正在更新: {url}")
ret = None
for i in range(0, self._retry_times):
ret = RequestUtils(accept_type="text/html",
ret = RequestUtils(accept_type="text/html", timeout=self._timeout,
proxies=settings.PROXY if self._proxy else None
).get_res(url)
if ret:
@@ -1263,6 +1360,8 @@ class ClashRuleProvider(_PluginBase):
logger.info(f"已更新: {url}. 节点数量: {len(rs['proxies'])}")
if rs.get('rules') is None:
rs['rules'] = []
if self._discard_proxy_groups:
rs['proxy-groups'] = []
rs = self.__remove_nodes_by_keywords(rs)
except Exception as e:
logger.error(f"解析配置出错: {e}")
@@ -1284,13 +1383,16 @@ class ClashRuleProvider(_PluginBase):
"""
通知 Clash 刷新规则集
"""
url = f'{self._clash_dashboard_url}/providers/rules/{ruleset}'
RequestUtils(content_type="application/json",
headers={"authorization": f"Bearer {self._clash_dashboard_secret}"}
).put(url)
for clash_dashboard in self._clash_dashboards:
clash_dashboard_url = clash_dashboard.get('url', '')
clash_dashboard_secret = clash_dashboard.get('secret', '')
url = f'{clash_dashboard_url}/providers/rules/{ruleset}'
RequestUtils(content_type="application/json", timeout=self._timeout,
headers={"authorization": f"Bearer {clash_dashboard_secret}"}
).put(url)
def proxy_groups_by_region(self) -> List[Dict[str, Any]]:
return ClashRuleProvider.__group_by_region(self._countries, self.all_proxies())
return self.__group_by_region(self._countries, self.all_proxies())
@staticmethod
def __load_countries(file_path: str) -> List:
@@ -1301,40 +1403,71 @@ class ClashRuleProvider(_PluginBase):
return []
return countries
@staticmethod
def __group_by_region(countries: List, proxies) -> List[Dict[str, Any]]:
continents_nodes = {'Asia': [], 'Europe': [], 'SouthAmerica': [], 'NorthAmerica': [], 'Africa': [],
'Oceania': [], 'AsiaExceptChina': []}
def __group_by_region(self, countries: List, proxies) -> List[Dict[str, Any]]:
continent_groups = {}
country_groups = {}
continent_map = {
'欧洲': 'Europe',
'亚洲': 'Asia',
'大洋洲': 'Oceania',
'非洲': 'Africa',
'北美洲': 'NorthAmerica',
'南美洲': 'SouthAmerica'
}
proxy_groups = []
hk = next(filter(lambda c: c['abbr'] == 'HK', countries),
{"abbr": "HK", "chinese": "中国香港特别行政区", "emoji": "🇭🇰"})
tw = next(filter(lambda c: c['abbr'] == 'TW', countries),
{"abbr": "TW", "chinese": "中国台湾", "emoji": "🇹🇼"})
for proxy_node in proxies:
continent = ClashRuleProvider.__continent_name_from_node(countries, proxy_node['name'])
if not continent:
country = ClashRuleProvider.__country_from_node(countries, proxy_node['name'])
if not country:
continue
continents_nodes[continent].append(proxy_node['name'])
for continent_nodes in continents_nodes:
if len(continents_nodes[continent_nodes]):
proxy_group = {'name': continent_nodes, 'type': 'select', 'proxies': continents_nodes[continent_nodes]}
if country.get("abbr") == "CN":
if any(key in proxy_node["name"] for key in ("🇭🇰", "HK", "香港")):
country = hk
if any(key in proxy_node["name"] for key in ("🇹🇼", "TW", "台湾")):
country = tw
continent = continent_map[country.get('continent')]
if self._group_by_region:
continent_groups.setdefault(continent, []).append(proxy_node['name'])
if self._group_by_country:
country_groups.setdefault(f"{country.get('emoji')} {country.get('chinese')}", []).append(
proxy_node['name'])
for continent, nodes in continent_groups.items():
if len(nodes):
proxy_group = {'name': continent, 'type': 'select', 'proxies': nodes}
proxy_groups.append(proxy_group)
for continent_node in continents_nodes['Asia']:
if any(x in continent_node for x in ('中国', '香港', 'CN')):
excluded = ('中国', '香港', 'CN', 'HK', '🇨🇳', '🇭🇰')
for continent_node in continent_groups.get('Asia', []):
if any(x in continent_node for x in excluded):
continue
continents_nodes['AsiaExceptChina'].append(continent_node)
if continents_nodes['AsiaExceptChina']:
proxy_group = {'name': 'AsiaExceptChina', 'type': 'select', 'proxies': continents_nodes['AsiaExceptChina']}
continent_groups.setdefault('AsiaExceptChina', []).append(continent_node)
if continent_groups.get('AsiaExceptChina'):
proxy_group = {'name': 'AsiaExceptChina', 'type': 'select', 'proxies': continent_groups['AsiaExceptChina']}
proxy_groups.append(proxy_group)
for country, nodes in country_groups.items():
if len(nodes):
proxy_group = {'name': country, 'type': 'select', 'proxies': nodes}
proxy_groups.append(proxy_group)
country_group = list(country_groups.keys())
if country_group:
proxy_groups.append({'name': '🏴‍☠️国家分组', 'type': 'select', 'proxies': country_group})
return proxy_groups
@staticmethod
def __continent_name_from_node(countries: List[Dict[str, str]], node_name: str) -> Optional[str]:
continents_names = {'欧洲': 'Europe',
'亚洲': 'Asia',
'大洋洲': 'Oceania',
'非洲': 'Africa',
'北美洲': 'NorthAmerica',
'南美洲': 'SouthAmerica'}
def __country_from_node(countries: List[Dict[str, str]], node_name: str) -> Optional[Dict[str, str]]:
node_name_lower = node_name.lower()
for country in countries:
if country['chinese'] in node_name or country['english'].lower() in node_name.lower():
return continents_names[country['continent']]
if country['emoji'] and country['emoji'] in node_name:
return country
elif (
country['chinese'] in node_name
or country['english'].lower() in node_name_lower
):
return country
return None
def __add_notification_job(self, ruleset_names: List[str]):
@@ -1376,7 +1509,7 @@ class ClashRuleProvider(_PluginBase):
for index, url in enumerate(self._sub_links):
config = self._clash_configs.get(url, {})
all_proxies.extend(config.get("proxies", []))
all_proxies.extend(self._clash_template.get("proxies", []))
all_proxies.extend(self._clash_template_dict.get("proxies", []))
all_proxies.extend(self._extra_proxies)
return all_proxies
@@ -1393,6 +1526,27 @@ class ClashRuleProvider(_PluginBase):
to_list.append(item)
return to_list
@staticmethod
def remove_invalid_outbounds(proxies: List[Dict[str, Any]], proxy_groups: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
从代理组中移除无效的出站
"""
outbounds = {proxy.get('name') for proxy in proxies if proxy.get('name')} | \
{proxy_group.get('name') for proxy_group in proxy_groups if proxy_group.get('name')} | \
{action.value for action in Action}
outbounds.add('GLOBAL')
for proxy_group in proxy_groups:
ps = []
if proxy_group.get('proxies'):
for proxy in proxy_group.get('proxies', []):
if proxy in outbounds:
ps.append(proxy)
else:
logger.warn(f"Proxy {proxy!r} in {proxy_group.get('name')!r} doesn't exist. Skipping...")
proxy_group['proxies'] = ps
return proxy_groups
def clash_config(self) -> Optional[Dict[str, Any]]:
"""
整理 clash 配置,返回配置字典
@@ -1400,13 +1554,13 @@ class ClashRuleProvider(_PluginBase):
# 使用模板或第一个订阅
first_config = self._clash_configs.get(self._sub_links[0], {}) if self._sub_links else {}
proxies = []
if not self._clash_template:
if not self._clash_template_dict:
clash_config = copy.deepcopy(first_config)
clash_config['proxy-groups'] = []
clash_config['rule-providers'] = {}
clash_config['rules'] = []
else:
clash_config = copy.deepcopy(self._clash_template)
clash_config = copy.deepcopy(self._clash_template_dict)
clash_config['proxy-groups'] = ClashRuleProvider.extend_with_name_checking(clash_config.get('proxy-groups', []),
first_config.get('proxy-groups', []),
)
@@ -1432,12 +1586,14 @@ class ClashRuleProvider(_PluginBase):
clash_config['proxy-groups'] = ClashRuleProvider.extend_with_name_checking(clash_config['proxy-groups'],
proxy_groups)
# 添加按大洲代理组
if self._group_by_region:
if self._group_by_region or self._group_by_country:
groups_by_region = self.proxy_groups_by_region()
if groups_by_region:
clash_config['proxy-groups'] = ClashRuleProvider.extend_with_name_checking(clash_config['proxy-groups'],
groups_by_region)
groups_by_region)
# 移除无效出站, 避免配置错误
clash_config['proxy-groups'] = ClashRuleProvider.remove_invalid_outbounds(clash_config.get('proxies', []),
clash_config.get('proxy-groups', []))
top_rules = []
outbound_names = list(x.get("name") for x in self.clash_outbound())
@@ -1454,7 +1610,7 @@ class ClashRuleProvider(_PluginBase):
path_name = hashlib.sha256(action_str.encode('utf-8')).hexdigest()[:10]
self._ruleset_names[path_name] = rule_provider_name
sub_url = (f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/ruleset?"
f"name={path_name}&apikey={settings.API_TOKEN}")
f"name={path_name}&apikey={self._apikey or settings.API_TOKEN}")
self._rule_provider[rule_provider_name] = {"behavior": "classical",
"format": "yaml",
"interval": 3600,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
.plugin-config[data-v-929102b8] {
margin: 0 auto;
}

View File

@@ -0,0 +1,4 @@
.plugin-config[data-v-3939efa1] {
margin: 0 auto;
}

View File

@@ -1,50 +0,0 @@
.plugin-page[data-v-d6db167c] {
margin: 0 auto;
}
/* 使卡片等宽并适应移动端 */
.d-flex.flex-wrap[data-v-d6db167c] {
gap: 16px;
}
.url-display[data-v-d6db167c] {
word-break: break-all;
padding: 8px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
/* 移动端堆叠布局 */
@media (max-width: 768px) {
.d-flex.flex-wrap[data-v-d6db167c] {
flex-direction: column;
}
}
/* Add visual distinction between sections */
.ruleset-section[data-v-d6db167c] {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
background-color: #f5f5f5;
}
.top-section[data-v-d6db167c] {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 16px;
background-color: #f9f9f9;
}
/* Optional: Add different border colors to further distinguish */
.ruleset-section[data-v-d6db167c] {
border-left: 4px solid #2196F3; /* Blue accent */
}
.top-section[data-v-d6db167c] {
border-left: 4px solid #4CAF50; /* Green accent */
}
.drag-handle[data-v-d6db167c] {
cursor: move;
}
.gap-2[data-v-d6db167c] {
gap: 8px;
}

View File

@@ -0,0 +1,69 @@
.plugin-page[data-v-ad6ce99d] {
margin: 0 auto;
}
/* 使卡片等宽并适应移动端 */
.d-flex.flex-wrap[data-v-ad6ce99d] {
gap: 16px;
}
/* 移动端堆叠布局 */
@media (max-width: 768px) {
.d-flex.flex-wrap[data-v-ad6ce99d] {
flex-direction: column;
}
}
.drag-handle[data-v-ad6ce99d] {
cursor: move;
}
.toggle-container[data-v-ad6ce99d] {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem;
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.subscription-card[data-v-ad6ce99d] {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.3s, box-shadow 0.3s;
background: white;
}
.subscription-card[data-v-ad6ce99d]:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.card-title[data-v-ad6ce99d] {
color: whitesmoke;
}
.card-header[data-v-ad6ce99d] {
padding: 0.625rem;
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 1) 0%, rgba(var(--v-theme-primary), 0.7) 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-refresh-button[data-v-ad6ce99d] {
background-color: rgba(var(--v-theme-primary), 0.9);
color: whitesmoke;
border: none;
border-radius: 6px;
padding: 0.625rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.3s;
}
.search-field[data-v-ad6ce99d] {
max-width: 25rem;
}
.clash-data-table[data-v-ad6ce99d] {
max-height: 40rem;
overflow-y: auto;
}

View File

@@ -2,11 +2,11 @@ const currentImports = {};
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
let moduleMap = {
"./Page":()=>{
dynamicLoadingCss(["__federation_expose_Page-BOym_1fV.css"], false, './Page');
return __federation_import('./__federation_expose_Page-D5l2MyNA.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Page-efdkIdKV.css"], false, './Page');
return __federation_import('./__federation_expose_Page-DEabfqvu.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Config":()=>{
dynamicLoadingCss(["__federation_expose_Config-BrXQaadr.css"], false, './Config');
return __federation_import('./__federation_expose_Config-NH09p1Am.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
dynamicLoadingCss(["__federation_expose_Config-CkZHWVJE.css"], false, './Config');
return __federation_import('./__federation_expose_Config-yV-dGVgm.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Dashboard":()=>{
dynamicLoadingCss(["__federation_expose_Dashboard-vS9Qm2ZB.css"], false, './Dashboard');
return __federation_import('./__federation_expose_Dashboard-BDSt5WaH.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};

View File

@@ -29,7 +29,7 @@ class ImdbSource(_PluginBase):
# 插件图标
plugin_icon = "IMDb_IOS-OSX_App.png"
# 插件版本
plugin_version = "1.5.6"
plugin_version = "1.5.8"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -60,7 +60,7 @@ class ImdbSource(_PluginBase):
def init_plugin(self, config: dict = None):
plugin_instance = self
plugin_instance: ImdbSource = self
def patched_recognize_media(chain_self, meta: MetaBase = None,
mtype: Optional[MediaType] = None,
@@ -822,6 +822,14 @@ class ImdbSource(_PluginBase):
ChainBase.async_recognize_media._patched_by == id(self) and
self._original_async_method):
ChainBase.async_recognize_media = self._original_async_method
if self._scheduler:
try:
self._scheduler.remove_all_jobs()
if self._scheduler.running:
self._scheduler.shutdown()
self._scheduler = None
except Exception as e:
logger.error(f"退出插件失败:{e}")
def get_module(self) -> Dict[str, Any]:
"""

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@
![](https://images2.imgbox.com/d6/b6/kZu6EH2a_o.png)
![](https://images2.imgbox.com/c8/3a/rEJBWu5v_o.png)
![](https://images2.imgbox.com/97/b7/d6RXFtwD_o.png)
![](https://images2.imgbox.com/8a/d4/AtgOe265_o.jpg)
# Gemini
@@ -38,7 +39,7 @@ CEFR全称是Common European Framework of Reference for Languages。
# 计划
- 双语字幕支持
- 考试词汇标注
- ~~考试词汇标注~~
# FAQ

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
pysubs2~=1.8.0
langdetect~=1.0.9
pymediainfo~=7.0.1
pymediainfo~=7.0.1
thinc==8.3.4
spacy==3.8.7

View File

@@ -0,0 +1,84 @@
from multiprocessing import Process, Queue
from typing import Dict, List
import spacy
from spacy.tokenizer import Tokenizer
from app.core.cache import cached
from app.log import logger
class SpacyWorker:
def __init__(self, model='en_core_web_sm'):
self.task_q = Queue()
self.result_q = Queue()
self.status_q = Queue()
self.model = model
# 启动子进程
logger.info(f"正在启动 SpacyWorker 子进程...")
self.proc = Process(target=self.run, args=(self.model,))
self.proc.start()
# 等待子进程返回模型加载状态
status, info = self.status_q.get()
if status == 'error':
self.proc.join()
raise RuntimeError(f"spaCy 模型加载失败: {info}")
else:
logger.info(f"spaCy 模型 `{self.model}` 加载成功")
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def run(self, model: str):
try:
nlp = SpacyWorker.load_nlp(model)
infixes = list(nlp.Defaults.infixes)
infixes = [i for i in infixes if '-' not in i]
infix_re = spacy.util.compile_infix_regex(infixes)
nlp.tokenizer = Tokenizer(
nlp.vocab,
prefix_search=nlp.tokenizer.prefix_search,
suffix_search=nlp.tokenizer.suffix_search,
infix_finditer=infix_re.finditer,
token_match=nlp.tokenizer.token_match
)
except Exception as e:
self.status_q.put(('error', str(e)))
return
# 告诉主进程加载成功
self.status_q.put(('ok', None))
while True:
text = self.task_q.get()
if text is None:
break
doc = nlp(text)
self.result_q.put([{'text': token.text, 'pos_': token.pos_, 'lemma_': token.lemma_} for token in doc])
@staticmethod
@cached(maxsize=1, ttl=3600 * 6)
def load_nlp(model: str) -> spacy.Language:
return spacy.load(model)
def submit(self, text: str) -> List[Dict[str, str]]:
"""
提交任务并等待结果
"""
self.task_q.put(text)
return self.result_q.get()
def close(self):
"""
关闭子进程
"""
if self.proc.is_alive():
self.task_q.put(None)
self.proc.join()
logger.info(f"SpacyWorker 子进程退出")

View File

@@ -0,0 +1,326 @@
from pathlib import Path
from typing import Any, List, Dict, Tuple
from app.core.context import MediaInfo
from app.core.event import eventmanager, Event
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import ChainEventType, MediaType, NotificationType
class MultiClass(_PluginBase):
# 插件名称
plugin_name = "视频多级分类"
# 插件描述
plugin_desc = "支持电影按照评分,年代和系列分类"
# 插件图标
plugin_icon = "Calibreweb_B.png"
# 插件版本
plugin_version = "0.1"
# 插件作者
plugin_author = "liuhangbin"
# 作者主页
author_url = "https://github.com/liuhangbin"
# 插件配置项ID前缀
plugin_config_prefix = "multiclass_"
# 加载顺序
plugin_order = 1
# 可使用的用户级别
auth_level = 1
_enabled = False
_notify = False
_year_class = False
_vote_class = False
_collection_class = False
def init_plugin(self, config: dict = None):
if config:
self._enabled = config.get("enabled", False)
self._notify = config.get("notify", False)
self._year_class = config.get("year_class", False)
self._vote_class = config.get("vote_class", False)
self._collection_class = config.get("collection_class", False)
def get_state(self) -> bool:
return self._enabled
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
"""
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'enabled',
'label': '启用插件',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'year_class',
'label': '按照年代分类',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'vote_class',
'label': '按照评分分类',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'collection_class',
'label': '按照系列分类',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 6
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'notify',
'label': '发送消息',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '插件目前仅支持电影(需要开启智能重命名)。如果按评分分类7-9 高分4-6 一般1-3 垃圾。 系列电影不参与评分, 不按年代分类。'
}
}
]
}
]
}
]
}
], {
"enabled": False,
"notify": False,
"year_class": False,
"vote_class": False,
"collection_class": False
}
def get_page(self) -> List[dict]:
pass
@eventmanager.register(ChainEventType.TransferRename)
def category_handler(self, event: Event):
"""
根据多级分类规则重新分类组装地址
"""
logger.debug(f"多级分类插件触发!")
# 基础验证
if not self.get_state():
logger.debug(f"多级分类插件未启用!")
return
if not event:
logger.warning(f"多级分类异常:事件对象为空")
return
if not hasattr(event, 'event_data'):
logger.warning(f"多级分类异常:事件数据为空")
return
try:
data = event.event_data
# 验证必要的数据字段
if not hasattr(data, 'render_str') or not data.render_str:
logger.warning(f"多级分类异常render_str为空")
return
else:
render_str = data.render_str
# 暂时只支持电影分类
if not hasattr(data, 'rename_dict') or not data.rename_dict:
logger.warning(f"多级分类异常rename_dict为空")
return
else:
rename_dict = data.rename_dict
video_type = rename_dict.get("type", "")
if video_type != "电影":
logger.debug(f"多级分类异常:不支持的媒体类型: {video_type}, 只支持电影分类")
return
# 安全获取数据字段
title = rename_dict.get("title", "")
en_title = rename_dict.get("en_title", "")
year = rename_dict.get("year")
vote_average = rename_dict.get("vote_average")
media_info = rename_dict.get("__mediainfo__")
# 初始化默认值
vote_count = 0
c_name = None
vote_path = "未知评分"
decade = 0
# 安全处理媒体信息
if media_info and hasattr(media_info, 'vote_count'):
try:
vote_count = int(media_info.vote_count) if media_info.vote_count else 0
except (ValueError, TypeError):
vote_count = 0
if hasattr(media_info, 'tmdb_info') and media_info.tmdb_info:
collection = media_info.tmdb_info.get("belongs_to_collection")
if collection and isinstance(collection, dict):
c_name = collection.get("name")
# 安全处理评分数据
try:
if vote_average is not None:
vote_average = float(vote_average)
else:
vote_average = 0
except (ValueError, TypeError):
vote_average = 0
# 评分分类逻辑
if vote_count < 10:
vote_average = 0
vote_path = "评分不足"
elif vote_average >= 7:
vote_path = "高分电影"
elif vote_average >= 4:
vote_path = "一般电影"
else:
vote_path = "垃圾电影"
# 安全处理年份数据
try:
if year and str(year).isdigit():
year_int = int(year)
if 1900 <= year_int <= 2100: # 合理的年份范围
decade = (year_int // 10) * 10
else:
decade = 0
logger.warning(f"年份超出合理范围: {year}")
else:
decade = 0
except (ValueError, TypeError):
decade = 0
logger.warning(f"年份转换失败: {year}")
# 构建分类路径
path_parts = []
if self._collection_class and c_name:
# 当collection为true时只添加collection name
# 清理collection名称移除特殊字符
clean_c_name = str(c_name).strip()
if clean_c_name:
path_parts.append("系列电影")
path_parts.append(clean_c_name)
else:
# 当collection不为true时根据其他配置添加路径
if self._vote_class and vote_path:
path_parts.append(vote_path)
if self._year_class and decade > 0:
path_parts.append(f"{decade}s")
# 构建最终的路径
if path_parts:
# 确保render_str不为空
safe_render_str = str(render_str).strip() if render_str else ""
event.event_data.updated_str = f"{'/'.join(path_parts)}/{safe_render_str}"
# 更新事件数据
event.event_data.updated = True
event.event_data.source = "MultiClass"
# 发送消息
if self._notify:
self.post_message(
mtype=NotificationType.Organize,
title="多级分类完成",
text=f"已重新分类: {event.event_data.updated_str}",
)
else:
event.event_data.updated = False
logger.warning(f"多级分类失败: 未找到分类路径,请检查配置是否已开启")
except Exception as e:
logger.error(f"多级分类异常: {str(e)}", exc_info=True)
# 确保即使出错也不会影响原始数据
if hasattr(event, 'event_data') and event.event_data:
event.event_data.updated = False
event.event_data.updated_str = getattr(data, 'render_str', '') if data else ''
def stop_service(self):
"""
停止服务
"""
pass

View File

@@ -38,7 +38,7 @@ class PersonMeta(_PluginBase):
# 插件图标
plugin_icon = "actor.png"
# 插件版本
plugin_version = "2.2"
plugin_version = "2.2.1"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -337,6 +337,9 @@ class PersonMeta(_PluginBase):
if not self._enabled:
return
# 事件数据
if not event or not event.event_data:
logger.warn("TransferComplete事件数据为空")
return
mediainfo: MediaInfo = event.event_data.get("mediainfo")
meta: MetaBase = event.event_data.get("meta")
if not mediainfo or not meta:
@@ -406,7 +409,7 @@ class PersonMeta(_PluginBase):
"""
peoples = []
# 更新当前媒体项人物
for people in iteminfo["People"] or []:
for people in iteminfo.get("People", []) or []:
if self._event.is_set():
logger.info(f"演职人员刮削服务停止")
return
@@ -488,7 +491,7 @@ class PersonMeta(_PluginBase):
if not seasons:
logger.warn(f"{item.title} 未找到季媒体项")
return
for season in seasons["Items"]:
for season in seasons.get("Items", []):
# 获取豆瓣演员信息
season_actors = self.__get_douban_actors(mediainfo=mediainfo, season=season.get("IndexNumber"))
# 如果是Jellyfin更新季的人物Emby/Plex季没有人物
@@ -514,7 +517,7 @@ class PersonMeta(_PluginBase):
logger.warn(f"{item.title} 未找到集媒体项")
continue
# 更新集媒体项人物
for episode in episodes["Items"]:
for episode in episodes.get("Items", []):
# 获取集媒体项详情
episodeinfo = self.get_iteminfo(server=server, server_type=server_type,
itemid=episode.get("Id"))
@@ -664,9 +667,13 @@ class PersonMeta(_PluginBase):
# 锁定人物信息
if updated_name:
if "LockedFields" not in personinfo:
personinfo["LockedFields"] = []
if "Name" not in personinfo["LockedFields"]:
personinfo["LockedFields"].append("Name")
if updated_overview:
if "LockedFields" not in personinfo:
personinfo["LockedFields"] = []
if "Overview" not in personinfo["LockedFields"]:
personinfo["LockedFields"].append("Overview")

View File

@@ -29,7 +29,7 @@ class ToBypassTrackers(_PluginBase):
# 插件图标
plugin_icon = "Clash_A.png"
# 插件版本
plugin_version = "1.4.2"
plugin_version = "1.4.3"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -68,7 +68,8 @@ class ToBypassTrackers(_PluginBase):
self.ipv6_txt = self.get_data("ipv6_txt") if self.get_data("ipv6_txt") else ""
self.ipv4_txt = self.get_data("ipv4_txt") if self.get_data("ipv4_txt") else ""
try:
with open(f"{settings.ROOT_PATH}/app/plugins/tobypasstrackers/sites/trackers", "r", encoding="utf-8") as f:
site_file = settings.ROOT_PATH/'app'/'plugins'/'tobypasstrackers'/'sites'/'trackers'
with open(site_file, "r", encoding="utf-8") as f:
base64_str = f.read()
self.trackers = json.loads(base64.b64decode(base64_str).decode("utf-8"))
except Exception as e:
@@ -101,7 +102,6 @@ class ToBypassTrackers(_PluginBase):
)
self._onlyonce = False
self.__update_config()
# self._scheduler.print_jobs()
self._scheduler.start()
def get_state(self) -> bool:
@@ -619,16 +619,14 @@ class ToBypassTrackers(_PluginBase):
# Load Chnroute6 Lists
res = RequestUtils().get_res(url=chnroute6_lists_url)
if res is not None and res.status_code == 200:
chnroute6_lists = res.text[:-1].split('\n')
for ipr in chnroute6_lists:
ipv6_list.append(ipr)
chnroute6_lists = res.text.strip().split('\n')
ipv6_list = [*chnroute6_lists]
if self._china_ip_route:
# Load Chnroute Lists
res = RequestUtils().get_res(url=chnroute_lists_url)
if res is not None and res.status_code == 200:
chnroute_lists = res.text[:-1].split('\n')
for ipr in chnroute_lists:
ip_list.append(ipr)
chnroute_lists = res.text.strip().split('\n')
ip_list = [*chnroute_lists]
do_sites = {site.domain: site.name for site in SiteOper().list_order_by_pri() if
site.id in self._bypassed_sites}
domain_name_map = {}

View File

@@ -1 +1 @@
eyJoZGRvbGJ5LmNvbSI6IFsidC5oZGRvbGJ5LmNvbSJdLCAidGp1cHQub3JnIjogWyJ0cmFja2VyLXB1YmxpYy50anVwdC5vcmciXSwgIm5pY2VwdC5uZXQiOiBbInd3dy5uaWNlcHQubmV0Il0sICJyb3VzaS56aXAiOiBbImhpdHB0LmNvbSJdLCAicHRob21lLm5ldCI6IFsicHRob21lLm5ldCJdLCAiaGR0aW1lLm9yZyI6IFsiaGR0aW1lLm9yZyJdLCAiZWFzdGdhbWUub3JnIjogWyJwdC5lYXN0Z2FtZS5vcmciXSwgInB0dGltZS5vcmciOiBbInd3dy5wdHRpbWUub3JnIl0sICJtLXRlYW0uY2MiOiBbInRyYWNrZXIubS10ZWFtLmNjIiwgInRyYWNrZXIubS10ZWFtLmlvIl0sICI1MnB0LnNpdGUiOiBbIjUycHQuc2l0ZSJdLCAicWluZ3dhcHQuY29tIjogWyJ0cmFja2VyLnFpbmd3YS5wcm8iLCAidHJhY2tlci5xaW5nd2FwdC5jb20iXSwgImhka3lsLmluIjogWyJ0cmFja2VyLmhka3lsLmluIl0sICJyYWluZ2ZoLnRvcCI6IFsicmFpbmdmaC50b3AiXSwgImhkZmFucy5vcmciOiBbImhkZmFucy5vcmciXSwgInB0bGdzLm9yZyI6IFsicHRsLmdzIiwgInJlbGF5MDEucHRsLmdzIl0sICJtb25pa2FkZXNpZ24udWsiOiBbInRyYWNrZXIubW9uaWthZGVzaWduLnVrIiwgImRhaWtpcmFpLm1vbmlrYWRlc2lnbi51ayIsICJhbmltZS1uby1pbmRleC5jb20iXSwgInB0c2Jhby5jbHViIjogWyJwdHNiYW8uY2x1YiJdLCAidG90aGVnbG9yeS5pbSI6IFsidHJhY2tlci50b3RoZWdsb3J5LmltIl0sICJ1Mi5kbWh5Lm9yZyI6IFsiZGF5ZHJlYW0uZG1oeS5iZXN0Il0sICJieXIucHQiOiBbInRyYWNrZXIuYnlyLnB0Il0sICJodWRidC5odXN0LmVkdS5jbiI6IFsiaHVkYnQuaHVzdC5lZHUuY24iXSwgImlsb2xpY29uLmNvbSI6IFsidHJhY2tlci5pbG9saWNvbi5jYyJdLCAiaGl0cHQuY29tIjogWyJoaXRwdC5jb20iXSwgImJ0c2Nob29sLmNsdWIiOiBbInB0LmJ0c2Nob29sLmNsdWIiXSwgImhkYXJlYS5jbHViIjogWyJ0cmFja2VyLmhkYXJlYS5jbHViIl0sICJzcHJpbmdzdW5kYXkubmV0IjogWyJvbjYuc3ByaW5nc3VuZGF5Lm5ldCIsICJvbi5zcHJpbmdzdW5kYXkubmV0Il0sICJ6bXB0LmNjIjogWyJ6bXB0LmNjIl0sICJjYXJwdC5uZXQiOiBbInRyYWNrZXIuY2FycHQubmV0Il0sICJpY2MyMDIyLmNvbSI6IFsidHJhY2tlci5pY2MyMDIyLnh5eiJdLCAia2VlcGZyZHMuY29tIjogWyJ0cmFja2VyLmtlZXBmcmRzLmNvbSJdLCAicHR6b25lLnh5eiI6IFsicHR6b25lLnh5eiJdLCAiY3NwdC50b3AiOiBbInRyYWNrZXIuY3NwdC50b3AiLCAidHJhY2tlci5jc3B0LmNjIiwgInRyYWNrZXIuY3NwdC5kYXRlIl0sICJjcmFicHQudmlwIjogWyJjcmFicHQudmlwIl0sICJva3B0Lm5ldCI6IFsid3d3Lm9rcHQubmV0Il0sICJnYW1lZ2FtZXB0LmNvbSI6IFsid3d3LmdhbWVnYW1lcHQuY29tIl0sICJhdWRpZW5jZXMubWUiOiBbInQuYXVkaWVuY2VzLm1lIiwgInRyYWNrZXIuY2luZWZpbGVzLmluZm8iXSwgInhpbmd5dW5nZS50b3AiOiBbInRyYWNrZXIueGluZ3l1bmdlLnRvcCIsICJ0cmFja2VyLnhpbmd5dW5nZS5zYnMiXSwgImV0OC5vcmciOiBbImV0OC5vcmciLCAidC5ldDgub3JnIl0sICJkaXNjZmFuLm5ldCI6IFsiZGlzY2Zhbi54eXoiXX0=
eyI1MnB0LnNpdGUiOiBbIjUycHQuc2l0ZSJdLCAiYXVkaWVuY2VzLm1lIjogWyJ0LmF1ZGllbmNlcy5tZSIsICJ0cmFja2VyLmNpbmVmaWxlcy5pbmZvIl0sICJidHNjaG9vbC5jbHViIjogWyJwdC5idHNjaG9vbC5jbHViIl0sICJieXIucHQiOiBbInRyYWNrZXIuYnlyLnB0Il0sICJjYXJwdC5uZXQiOiBbInRyYWNrZXIuY2FycHQubmV0Il0sICJjcmFicHQudmlwIjogWyJjcmFicHQudmlwIl0sICJjc3B0LnRvcCI6IFsidHJhY2tlci5jc3B0LnRvcCIsICJ0cmFja2VyLmNzcHQuY2MiLCAidHJhY2tlci5jc3B0LmRhdGUiXSwgImRpc2NmYW4ubmV0IjogWyJkaXNjZmFuLnh5eiJdLCAiZWFzdGdhbWUub3JnIjogWyJwdC5lYXN0Z2FtZS5vcmciXSwgImV0OC5vcmciOiBbImV0OC5vcmciLCAidC5ldDgub3JnIl0sICJnYW1lZ2FtZXB0LmNvbSI6IFsid3d3LmdhbWVnYW1lcHQuY29tIl0sICJoZGFyZWEuY2x1YiI6IFsidHJhY2tlci5oZGFyZWEuY2x1YiJdLCAiaGRkb2xieS5jb20iOiBbInQuaGRkb2xieS5jb20iXSwgImhkZmFucy5vcmciOiBbImhkZmFucy5vcmciXSwgImhka3lsLmluIjogWyJ0cmFja2VyLmhka3lsLmluIl0sICJoZHRpbWUub3JnIjogWyJoZHRpbWUub3JnIl0sICJoaXRwdC5jb20iOiBbImhpdHB0LmNvbSJdLCAiaHVkYnQuaHVzdC5lZHUuY24iOiBbImh1ZGJ0Lmh1c3QuZWR1LmNuIl0sICJpY2MyMDIyLmNvbSI6IFsidHJhY2tlci5pY2MyMDIyLnh5eiJdLCAiaWxvbGljb24uY29tIjogWyJ0cmFja2VyLmlsb2xpY29uLmNjIl0sICJrZWVwZnJkcy5jb20iOiBbInRyYWNrZXIua2VlcGZyZHMuY29tIl0sICJtLXRlYW0uY2MiOiBbInRyYWNrZXIubS10ZWFtLmNjIiwgInRyYWNrZXIubS10ZWFtLmlvIl0sICJtb25pa2FkZXNpZ24udWsiOiBbInRyYWNrZXIubW9uaWthZGVzaWduLnVrIiwgImRhaWtpcmFpLm1vbmlrYWRlc2lnbi51ayIsICJhbmltZS1uby1pbmRleC5jb20iXSwgIm5pY2VwdC5uZXQiOiBbInd3dy5uaWNlcHQubmV0Il0sICJva3B0Lm5ldCI6IFsid3d3Lm9rcHQubmV0Il0sICJwdGhvbWUubmV0IjogWyJwdGhvbWUubmV0Il0sICJwdGxncy5vcmciOiBbInB0bC5ncyIsICJyZWxheTAxLnB0bC5ncyJdLCAicHRzYmFvLmNsdWIiOiBbInB0c2Jhby5jbHViIl0sICJwdHRpbWUub3JnIjogWyJ3d3cucHR0aW1lLm9yZyJdLCAicHR6b25lLnh5eiI6IFsicHR6b25lLnh5eiJdLCAicWluZ3dhcHQuY29tIjogWyJ0cmFja2VyLnFpbmd3YS5wcm8iLCAidHJhY2tlci5xaW5nd2FwdC5jb20iXSwgInJhaW5nZmgudG9wIjogWyJyYWluZ2ZoLnRvcCJdLCAicm91c2kuemlwIjogWyJoaXRwdC5jb20iXSwgInNwcmluZ3N1bmRheS5uZXQiOiBbIm9uNi5zcHJpbmdzdW5kYXkubmV0IiwgIm9uLnNwcmluZ3N1bmRheS5uZXQiXSwgInRqdXB0Lm9yZyI6IFsidHJhY2tlci1wdWJsaWMudGp1cHQub3JnIl0sICJ0b3RoZWdsb3J5LmltIjogWyJ0cmFja2VyLnRvdGhlZ2xvcnkuaW0iXSwgInUyLmRtaHkub3JnIjogWyJkYXlkcmVhbS5kbWh5LmJlc3QiXSwgInhpbmd5dW5nZS50b3AiOiBbInRyYWNrZXIueGluZ3l1bmdlLnRvcCIsICJ0cmFja2VyLnhpbmd5dW5nZS5zYnMiXSwgInptcHQuY2MiOiBbInptcHQuY2MiXSwgImhoYW5jbHViLnRvcCI6IFsidHJhY2tlci5oaGFuY2x1Yi50b3AiXSwgImhkY2l0eS5jaXR5IjogWyJzeW5jLmxlbml0ZXIub3JnIl19

View File

@@ -4,7 +4,6 @@ from abc import ABCMeta, abstractmethod
from typing import Tuple
import chardet
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.helper.browser import PlaywrightHelper
@@ -33,7 +32,7 @@ class _ISiteSigninHandler(metaclass=ABCMeta):
return False
@abstractmethod
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -26,7 +24,7 @@ class BTSchool(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -3,7 +3,6 @@ import re
from typing import Tuple
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
@@ -36,7 +35,7 @@ class CHDBits(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -26,7 +24,7 @@ class HaiDan(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,8 +1,6 @@
import json
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
@@ -29,7 +27,7 @@ class Hares(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
@@ -30,7 +28,7 @@ class HDArea(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -2,7 +2,6 @@ import json
from typing import Tuple
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
@@ -30,7 +29,7 @@ class HDChina(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -28,7 +26,7 @@ class HDCity(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -2,8 +2,6 @@ import json
import time
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.helper.ocr import OcrHelper
from app.log import logger
@@ -31,7 +29,7 @@ class HDSky(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,8 +1,6 @@
import re
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -30,7 +28,7 @@ class HDUpt(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,4 @@
from typing import Tuple
from urllib.parse import urljoin
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.plugins.autosignin.sites import _ISiteSigninHandler
@@ -25,7 +22,7 @@ class MTorrent(_ISiteSigninHandler):
"""
return True if cls.site_url in url.split(".") else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作,馒头实际没有签到,非仿真模式下需要更新访问时间
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息
@@ -52,7 +49,7 @@ class MTorrent(_ISiteSigninHandler):
else:
return False, "模拟登录失败,无法打开网站"
def login(self, site_info: CommentedMap) -> Tuple[bool, str]:
def login(self, site_info: dict) -> Tuple[bool, str]:
"""
执行登录操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
@@ -30,7 +28,7 @@ class NexusHD(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -3,7 +3,6 @@ import time
from typing import Tuple
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.helper.ocr import OcrHelper
@@ -32,7 +31,7 @@ class Opencd(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,8 +1,6 @@
import json
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -24,7 +22,7 @@ class PTerClub(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,7 +1,5 @@
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils
@@ -26,7 +24,7 @@ class PTTime(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -6,7 +6,6 @@ from typing import Tuple
from PIL import Image
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
@@ -46,7 +45,7 @@ class Tjupt(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,8 +1,6 @@
import re
from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
@@ -33,7 +31,7 @@ class TTG(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -4,7 +4,6 @@ import re
from typing import Tuple
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
@@ -39,7 +38,7 @@ class U2(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -1,8 +1,6 @@
from typing import Tuple
from urllib.parse import urljoin
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.http import RequestUtils
@@ -24,7 +22,7 @@ class YemaPT(_ISiteSigninHandler):
"""
return True if cls.site_url in url else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息
@@ -50,7 +48,7 @@ class YemaPT(_ISiteSigninHandler):
else:
return False, "签到失败,无法打开网站"
def login(self, site_info: CommentedMap) -> Tuple[bool, str]:
def login(self, site_info: dict) -> Tuple[bool, str]:
"""
执行登录操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -2,7 +2,6 @@ import json
from typing import Tuple
from lxml import etree
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
@@ -27,7 +26,7 @@ class ZhuQue(_ISiteSigninHandler):
"""
return True if StringUtils.url_equal(url, cls.site_url) else False
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
def signin(self, site_info: dict) -> Tuple[bool, str]:
"""
执行签到操作
:param site_info: 站点信息含有站点Url、站点Cookie、UA等信息

View File

@@ -21,7 +21,7 @@ class DingdingMsg(_PluginBase):
# 插件图标
plugin_icon = "Dingding_A.png"
# 插件版本
plugin_version = "1.12"
plugin_version = "1.13"
# 插件作者
plugin_author = "nnlegenda"
# 作者主页
@@ -209,6 +209,8 @@ class DingdingMsg(_PluginBase):
if text:
# 对text进行Markdown特殊字符转义
text = re.sub(r"([_`])", r"\\\1", text)
# 钉钉中需要在换行前有两个空格,才能够正常换行
text = re.sub(r"\n", r" \n", text)
else:
text = ""