mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-13 15:09:12 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
BIN
icons/Mihomo_Meta_A.png
Executable file
BIN
icons/Mihomo_Meta_A.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
15
package.json
15
package.json
@@ -25,7 +25,8 @@
|
||||
"AutoSubv2": {
|
||||
"name": "AI字幕自动生成(v2)",
|
||||
"description": "使用whisper自动生成视频文件字幕,使用大模型翻译字幕成中文。",
|
||||
"version": "1.2",
|
||||
"labels": "字幕",
|
||||
"version": "2.2",
|
||||
"icon": "autosubtitles.jpeg",
|
||||
"author": "TimoYoung",
|
||||
"level": 1,
|
||||
@@ -33,7 +34,10 @@
|
||||
"history": {
|
||||
"v1.0": "first stable version",
|
||||
"v1.1": "优化字幕翻译逻辑,优化日志输出",
|
||||
"v1.2": "fix openai_proxy打开时,翻译失败的问题,优化日志输出"
|
||||
"v1.2": "fix openai_proxy打开时,翻译失败的问题,优化日志输出",
|
||||
"v2.0": "1.引入任务队列 2.支持监听媒体入库自动生成字幕 3.增加任务状态展示界面",
|
||||
"v2.1": "支持清除历史记录",
|
||||
"v2.2": "fix"
|
||||
}
|
||||
},
|
||||
"CustomSites": {
|
||||
@@ -796,11 +800,14 @@
|
||||
"name": "ntfy消息推送",
|
||||
"description": "支持使用ntfy发送消息通知。",
|
||||
"labels": "消息通知",
|
||||
"version": "1.0",
|
||||
"version": "1.1",
|
||||
"icon": "Ntfy_A.png",
|
||||
"author": "lethargicScribe",
|
||||
"level": 1,
|
||||
"v2": true
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.1": "添加Token认证和用户动作"
|
||||
}
|
||||
},
|
||||
"GotifyMsg": {
|
||||
"name": "gotify消息推送",
|
||||
|
||||
@@ -425,15 +425,28 @@
|
||||
"name": "IMDb源",
|
||||
"description": "让探索支持IMDb数据源。",
|
||||
"labels": "探索",
|
||||
"version": "1.3",
|
||||
"version": "1.3.1",
|
||||
"icon": "IMDb_IOS-OSX_App.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.3.1": "修复按日期排序错误",
|
||||
"v1.3": "优化网络连接",
|
||||
"v1.2": "推荐热门纪录片",
|
||||
"v1.1": "推荐支持IMDB数据源; 优化海报尺寸,减少卡顿",
|
||||
"v1.0": "探索支持IMDb数据源"
|
||||
}
|
||||
},
|
||||
"ClashRuleProvider": {
|
||||
"name": "Clash Rule Provider",
|
||||
"description": "随时为Clash添加一些额外的规则。",
|
||||
"labels": "工具",
|
||||
"version": "0.1.0",
|
||||
"icon": "Mihomo_Meta_A.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v0.1.0": "新增ClashRuleProvider"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
625
plugins.v2/clashruleprovider/__init__.py
Normal file
625
plugins.v2/clashruleprovider/__init__.py
Normal file
@@ -0,0 +1,625 @@
|
||||
import requests
|
||||
import re
|
||||
from typing import Any, Optional, List, Dict, Tuple, Union
|
||||
import time
|
||||
import yaml
|
||||
import hashlib
|
||||
from fastapi import Body, Response
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from cachetools import cached, TTLCache
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import ClashRuleParser
|
||||
from app.plugins.clashruleprovider.clash_rule_parser import Action, RuleType, ClashRule, MatchRule, LogicRule
|
||||
|
||||
|
||||
class ClashRuleProvider(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "Clash Rule Provider"
|
||||
# 插件描述
|
||||
plugin_desc = "随时为Clash添加一些额外的规则。"
|
||||
# 插件图标
|
||||
plugin_icon = ("https://raw.githubusercontent.com/wumode/MoviePilot-Plugins/"
|
||||
"refs/heads/imdbsource_assets/icons/Mihomo_Meta_A.png")
|
||||
# 插件版本
|
||||
plugin_version = "0.1.0"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/wumode"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "clashruleprovider_"
|
||||
# 加载顺序
|
||||
plugin_order = 99
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 插件配置
|
||||
# 启用插件
|
||||
_enabled = False
|
||||
_proxy = False
|
||||
_notify = False
|
||||
# 订阅链接
|
||||
_sub_links = []
|
||||
# Clash 面板 URL
|
||||
_clash_dashboard_url = None
|
||||
# Clash 面板密钥
|
||||
_clash_dashboard_secret = None
|
||||
# MoviePilot URL
|
||||
_movie_pilot_url = None
|
||||
_cron = ''
|
||||
_timeout = 10
|
||||
_retry_times = 3
|
||||
_filter_keywords = []
|
||||
_auto_update_subscriptions = True
|
||||
_ruleset_prefix = '📂<-'
|
||||
|
||||
# 插件数据
|
||||
_clash_config = None
|
||||
_top_rules: List[str] = []
|
||||
_ruleset_rules: List[str] = []
|
||||
_rule_provider: Dict[str, Any] = {}
|
||||
_subscription_info = {}
|
||||
_ruleset_names: Dict[str, str] = {}
|
||||
|
||||
# protected variables
|
||||
_clash_rule_parser = None
|
||||
_ruleset_rule_parser = None
|
||||
_custom_rule_sets = None
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self._clash_config = self.get_data("clash_config")
|
||||
self._ruleset_rules = self.get_data("ruleset_rules")
|
||||
self._top_rules = self.get_data("top_rules")
|
||||
self._subscription_info = self.get_data("subscription_info") or \
|
||||
{"download": 0, "upload": 0, "total": 0, "expire": 0, "last_update": 0}
|
||||
self._rule_provider = self.get_data("rule_provider") or {}
|
||||
self._ruleset_names = self.get_data("ruleset_names") or {}
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._proxy = config.get("proxy")
|
||||
self._notify = config.get("notify"),
|
||||
self._sub_links = config.get("sub_links")
|
||||
self._clash_dashboard_url = config.get("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[-1] == '/':
|
||||
self._movie_pilot_url = self._movie_pilot_url[:-1]
|
||||
self._cron = config.get("cron_string")
|
||||
self._timeout = config.get("timeout")
|
||||
self._retry_times = config.get("retry_times")
|
||||
self._filter_keywords = config.get("filter_keywords")
|
||||
self._ruleset_prefix = config.get("ruleset_prefix", "Custom_")
|
||||
self._auto_update_subscriptions = config.get("auto_update_subscriptions")
|
||||
self._clash_rule_parser = ClashRuleParser()
|
||||
self._ruleset_rule_parser = ClashRuleParser()
|
||||
if self._enabled:
|
||||
self.__parse_config()
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
self._scheduler.start()
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"path": "/connectivity",
|
||||
"endpoint": self.test_connectivity,
|
||||
"methods": ["POST"],
|
||||
"auth": "bear",
|
||||
"summary": "测试连接",
|
||||
"description": "测试连接"
|
||||
},
|
||||
{
|
||||
"path": "/clash_outbound",
|
||||
"endpoint": self.get_clash_outbound,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash outbound",
|
||||
"description": "clash outbound"
|
||||
},
|
||||
{
|
||||
"path": "/status",
|
||||
"endpoint": self.get_status,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "stated",
|
||||
"description": "state"
|
||||
},
|
||||
{
|
||||
"path": "/rules",
|
||||
"endpoint": self.get_rules,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/rules",
|
||||
"endpoint": self.update_rules,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/reorder-rules",
|
||||
"endpoint": self.reorder_rules,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.update_rule,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.add_rule,
|
||||
"methods": ["POSt"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/rule",
|
||||
"endpoint": self.delete_rule,
|
||||
"methods": ["DELETE"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/subscription",
|
||||
"endpoint": self.get_subscription,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "clash rules",
|
||||
"description": "clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/subscription",
|
||||
"endpoint": self.update_subscription,
|
||||
"methods": ["PUT"],
|
||||
"auth": "bear",
|
||||
"summary": "update clash rules",
|
||||
"description": "update clash rules"
|
||||
},
|
||||
{
|
||||
"path": "/rule_providers",
|
||||
"endpoint": self.get_rule_providers,
|
||||
"methods": ["GET"],
|
||||
"auth": "bear",
|
||||
"summary": "update rule providers",
|
||||
"description": "update rule providers"
|
||||
},
|
||||
{
|
||||
"path": "/ruleset",
|
||||
"endpoint": self.get_ruleset,
|
||||
"methods": ["GET"],
|
||||
"summary": "update rule providers",
|
||||
"description": "update rule providers"
|
||||
},
|
||||
{
|
||||
"path": "/config",
|
||||
"endpoint": self.get_clash_config,
|
||||
"methods": ["GET"],
|
||||
"summary": "update rule providers",
|
||||
"description": "update rule providers"
|
||||
}
|
||||
]
|
||||
|
||||
def get_render_mode(self) -> Tuple[str, str]:
|
||||
"""
|
||||
获取插件渲染模式
|
||||
:return: 1、渲染模式,支持:vue/vuetify,默认vuetify
|
||||
:return: 2、组件路径,默认 dist/assets
|
||||
"""
|
||||
return "vue", "dist/assets"
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [], {}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
return []
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
if self.get_state() and self._auto_update_subscriptions:
|
||||
return [{
|
||||
"id": "ClashRuleProvider",
|
||||
"name": "Clash Rule Provider 服务",
|
||||
"trigger": CronTrigger.from_crontab(self._cron),
|
||||
"func": self.update_subscription_service,
|
||||
"kwargs": {}
|
||||
}]
|
||||
return []
|
||||
|
||||
def __update_config(self):
|
||||
# 保存配置
|
||||
self.update_config(
|
||||
{
|
||||
"enabled": self._enabled,
|
||||
"cron": self._cron,
|
||||
"proxy": self._proxy,
|
||||
"notify": self._notify,
|
||||
"sub_links": self._sub_links,
|
||||
"clash_dashboard_url": self._clash_dashboard_url,
|
||||
"clash_dashboard_secret": self._clash_dashboard_secret,
|
||||
"movie_pilot_url": self._movie_pilot_url,
|
||||
"retry_times": self._retry_times,
|
||||
"timeout": self._timeout,
|
||||
})
|
||||
|
||||
def __save_data(self):
|
||||
self.__insert_ruleset()
|
||||
self._top_rules = self._clash_rule_parser.to_string()
|
||||
self._ruleset_rules = self._ruleset_rule_parser.to_string()
|
||||
self.save_data('clash_config', self._clash_config)
|
||||
self.save_data('ruleset_rules', self._ruleset_rules)
|
||||
self.save_data('top_rules', self._top_rules)
|
||||
self.save_data('subscription_info', self._subscription_info)
|
||||
self.save_data('ruleset_names', self._ruleset_names)
|
||||
self.save_data('rule_provider', self._rule_provider)
|
||||
|
||||
def __parse_config(self):
|
||||
if not self._top_rules:
|
||||
return
|
||||
self._clash_rule_parser.parse_rules_from_list(self._top_rules)
|
||||
if not self._ruleset_rules:
|
||||
return
|
||||
self._ruleset_rule_parser.parse_rules_from_list(self._ruleset_rules)
|
||||
|
||||
def test_connectivity(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
if not params.get('clash_dashboard_url') or not params.get('clash_dashboard_secret')\
|
||||
or not params.get('sub_link'):
|
||||
return {"success": False, "message": "missing params"}
|
||||
clash_version_url = f"{params.get('clash_dashboard_url')}/version"
|
||||
ret = RequestUtils(accept_type="application/json",
|
||||
headers={"authorization": f"Bearer {params.get('clash_dashboard_secret')}"}
|
||||
).get(clash_version_url)
|
||||
if not ret:
|
||||
return {"success": False, "message": "无法连接到Clash"}
|
||||
ret = RequestUtils(accept_type="text/html",
|
||||
proxies=settings.PROXY if self._proxy else None
|
||||
).get(params.get('sub_link'))
|
||||
if not ret:
|
||||
return {"success": False, "message": f"Unable to get {params.get('sub_link')}"}
|
||||
return {"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)
|
||||
# if rules or ruleset in self._rule_provider:
|
||||
# self._rule_provider[ruleset] = rules
|
||||
res = yaml.dump({"payload": rules}, allow_unicode=True)
|
||||
return Response(content=res, media_type="text/yaml")
|
||||
|
||||
def get_clash_outbound(self):
|
||||
outbound = self.clash_outbound(self._clash_config)
|
||||
return {"success": True, "message": None, "data": {"outbound": outbound}}
|
||||
|
||||
def get_status(self):
|
||||
rule_size = len(self._clash_config.get("rules", [])) if self._clash_config else 0
|
||||
return {"success": True, "message": "",
|
||||
"data": {"state": self._enabled,
|
||||
"ruleset_prefix": self._ruleset_prefix,
|
||||
"clash": {"rule_size": rule_size},
|
||||
"subscription_info": self._subscription_info,
|
||||
"sub_url": f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/config?"
|
||||
f"apikey={settings.API_TOKEN}"}}
|
||||
|
||||
def get_clash_config(self):
|
||||
config = self.clash_config()
|
||||
if not config:
|
||||
return {"success": False, "message": ""}
|
||||
res = yaml.dump(config, allow_unicode=True)
|
||||
headers = {'Subscription-Userinfo': f'upload={self._subscription_info["upload"]}; '
|
||||
f'download={self._subscription_info["download"]}; '
|
||||
f'total={self._subscription_info["total"]}; '
|
||||
f'expire={self._subscription_info["expire"]}'}
|
||||
return Response(headers=headers, content=res, media_type="text/yaml")
|
||||
|
||||
def get_rules(self, rule_type: str) -> Dict[str, Any]:
|
||||
if rule_type == 'ruleset':
|
||||
return {"success": True, "message": None, "data": {"rules": self._ruleset_rule_parser.to_dict()}}
|
||||
return {"success": True, "message": None, "data": {"rules": self._clash_rule_parser.to_dict()}}
|
||||
|
||||
def delete_rule(self, params: dict = Body(...)):
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
if params.get('type') == 'ruleset':
|
||||
res = self.delete_rule_by_priority(params.get('priority'), self._ruleset_rule_parser)
|
||||
if res:
|
||||
self.__add_notification_job(f"{self._ruleset_prefix}{res.action.value if isinstance(res.action, Action) else res.action}")
|
||||
else:
|
||||
res = self.delete_rule_by_priority(params.get('priority'), self._clash_rule_parser)
|
||||
return {"success": res, "message": None}
|
||||
|
||||
def reorder_rules(self, params: Dict[str, Any]):
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
moved_priority = params.get('moved_priority')
|
||||
target_priority = params.get('target_priority')
|
||||
try:
|
||||
if params.get('type') == 'ruleset':
|
||||
self.__reorder_rules(self._ruleset_rule_parser, moved_priority, target_priority)
|
||||
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
|
||||
else:
|
||||
self.__reorder_rules(self._clash_rule_parser, moved_priority, target_priority)
|
||||
except Exception as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
return {"success": True, "message": None}
|
||||
|
||||
def update_rules(self, params: Dict[str, Any]):
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
if params.get('type') == 'ruleset':
|
||||
self.__update_rules(params.get('rules'), self._ruleset_rule_parser)
|
||||
else:
|
||||
self.__update_rules(params.get('rules'), self._clash_rule_parser)
|
||||
return {"success": True, "message": None}
|
||||
|
||||
def update_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
if params.get('type') == 'ruleset':
|
||||
res = self.update_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser)
|
||||
if res:
|
||||
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
|
||||
else:
|
||||
res = self.update_rule_by_priority(params.get('rule_data'), self._clash_rule_parser)
|
||||
return {"success": bool(res), "message": None}
|
||||
|
||||
def add_rule(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
if params.get('type') == 'ruleset':
|
||||
res = self.add_rule_by_priority(params.get('rule_data'), self._ruleset_rule_parser)
|
||||
if res:
|
||||
self.__add_notification_job(f"{self._ruleset_prefix}{params.get('rule_data').get('action')}")
|
||||
else:
|
||||
res = self.add_rule_by_priority(params.get('rule_data'), self._clash_rule_parser)
|
||||
return {"success": bool(res), "message": None}
|
||||
|
||||
def get_subscription(self):
|
||||
if not self._sub_links:
|
||||
return None
|
||||
return {"success": True, "message": None, "data": {"url": self._sub_links[0]}}
|
||||
|
||||
def update_subscription(self, params: Dict[str, Any]):
|
||||
if not self._enabled:
|
||||
return {"success": False, "message": ""}
|
||||
url = params.get('url')
|
||||
if not url:
|
||||
return {"success": False, "message": "missing params"}
|
||||
res = self.update_subscription_service()
|
||||
if not res:
|
||||
return {"success": True, "message": f"订阅链接 {self._sub_links[0]} 更新失败"}
|
||||
return {"success": True, "message": "订阅更新成功"}
|
||||
|
||||
def get_rule_providers(self):
|
||||
return {"success": True, "message": None, "data": self.rule_providers()}
|
||||
|
||||
@staticmethod
|
||||
def clash_outbound(clash_config: Dict[str, Any]) -> Optional[List]:
|
||||
if not clash_config:
|
||||
return []
|
||||
outbound = [{'name': proxy_group.get("name")} for proxy_group in clash_config.get("proxy-groups")]
|
||||
outbound.extend([{'name': proxy.get("name")} for proxy in clash_config.get("proxies")])
|
||||
return outbound
|
||||
|
||||
def rule_providers(self) -> Optional[Dict[str, Any]]:
|
||||
if not self._clash_config:
|
||||
return None
|
||||
rule_providers = {}
|
||||
for key, value in self._clash_config.get("rule-providers", {}):
|
||||
if value.get("path", '').startwith("./CRP/"):
|
||||
continue
|
||||
rule_providers[key] = value
|
||||
return rule_providers
|
||||
|
||||
def __update_rules(self, rules: List[Dict[str, Any]], rule_parser: ClashRuleParser):
|
||||
rule_parser.rules = []
|
||||
for rule in rules:
|
||||
clash_rule = ClashRuleParser.parse_rule_dict(rule)
|
||||
rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority"))
|
||||
self.__save_data()
|
||||
|
||||
def __reorder_rules(self, rule_parser: ClashRuleParser, moved_priority, target_priority):
|
||||
rule_parser.reorder_rules(moved_priority, target_priority)
|
||||
self.__save_data()
|
||||
|
||||
def __get_ruleset(self, ruleset: str) -> List[str]:
|
||||
if ruleset.startswith(self._ruleset_prefix):
|
||||
action = ruleset[len(self._ruleset_prefix):]
|
||||
else:
|
||||
return []
|
||||
try:
|
||||
action_enum = Action(action.upper())
|
||||
final_action = action_enum
|
||||
except ValueError:
|
||||
final_action = action
|
||||
rules = self._ruleset_rule_parser.filter_rules_by_action(final_action)
|
||||
res = []
|
||||
for rule in rules:
|
||||
res.append(rule.condition_string())
|
||||
return res
|
||||
|
||||
def __insert_ruleset(self):
|
||||
outbounds = []
|
||||
for rule in self._ruleset_rule_parser.rules:
|
||||
action_str = f"{rule.action.value}" if isinstance(rule.action, Action) else rule.action
|
||||
if action_str not in outbounds:
|
||||
outbounds.append(action_str)
|
||||
self._clash_rule_parser.remove_rules(lambda r: r.rule_type == RuleType.RULE_SET and
|
||||
r.payload.startswith(self._ruleset_prefix))
|
||||
for outbound in outbounds:
|
||||
clash_rule = ClashRuleParser.parse_rule_line(f"RULE-SET,{self._ruleset_prefix}{outbound},{outbound}")
|
||||
if not self._clash_rule_parser.has_rule(clash_rule):
|
||||
self._clash_rule_parser.insert_rule_at_priority(clash_rule, 0)
|
||||
|
||||
def update_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool:
|
||||
if not isinstance(rule.get("priority"), int):
|
||||
return False
|
||||
clash_rule = ClashRuleParser.parse_rule_dict(rule)
|
||||
if not clash_rule:
|
||||
return False
|
||||
res = rule_parser.update_rule_at_priority(clash_rule, rule.get("priority"))
|
||||
self.__save_data()
|
||||
return res
|
||||
|
||||
def add_rule_by_priority(self, rule: Dict[str, Any], rule_parser: ClashRuleParser) -> bool:
|
||||
if not isinstance(rule.get("priority"), int):
|
||||
return False
|
||||
try:
|
||||
clash_rule = self._clash_rule_parser.parse_rule_dict(rule)
|
||||
except ValueError:
|
||||
logger.warn(f"无效的输入规则: {rule}")
|
||||
return False
|
||||
if not clash_rule:
|
||||
return False
|
||||
rule_parser.insert_rule_at_priority(clash_rule, rule.get("priority"))
|
||||
self.__save_data()
|
||||
return True
|
||||
|
||||
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
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def update_subscription_service(self) -> bool:
|
||||
if not self._sub_links:
|
||||
return False
|
||||
url = self._sub_links[0]
|
||||
ret = RequestUtils(accept_type="text/html",
|
||||
proxies=settings.PROXY if self._proxy else None
|
||||
).get_res(url)
|
||||
if not ret:
|
||||
return False
|
||||
try:
|
||||
rs = yaml.load(ret.content, Loader=yaml.FullLoader)
|
||||
self._clash_config = self.__remove_nodes_by_keywords(rs)
|
||||
except Exception as e:
|
||||
logger.error(f"解析配置出错: {e}")
|
||||
return False
|
||||
if 'Subscription-Userinfo' in ret.headers:
|
||||
matches = re.findall(r'(\w+)=(\d+)', ret.headers['Subscription-Userinfo'])
|
||||
variables = {key: int(value) for key, value in matches}
|
||||
self._subscription_info['download'] = variables['download']
|
||||
self._subscription_info['upload'] = variables['upload']
|
||||
self._subscription_info['total'] = variables['total']
|
||||
self._subscription_info['expire'] = variables['expire']
|
||||
self._subscription_info["last_update"] = int(time.time())
|
||||
self.save_data('subscription_info', self._subscription_info)
|
||||
self.save_data('clash_config', self._clash_config)
|
||||
return True
|
||||
|
||||
def notify_clash(self, ruleset: str):
|
||||
url = f'{self._clash_dashboard_url}/providers/rules/{ruleset}'
|
||||
RequestUtils(content_type="application/json",
|
||||
headers={"authorization": f"Bearer {self._clash_dashboard_secret}"}
|
||||
).put(url)
|
||||
|
||||
def __add_notification_job(self, ruleset: str):
|
||||
if ruleset in self._rule_provider:
|
||||
self._scheduler.add_job(self.notify_clash, "date",
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=30),
|
||||
args=[ruleset],
|
||||
id='CRP-notify-clash',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
def __remove_nodes_by_keywords(self, clash_config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
removed_proxies = []
|
||||
proxies = []
|
||||
for proxy in clash_config.get("proxies", []):
|
||||
has_keywords = bool(len([x for x in self._filter_keywords if x in proxy.get("name", '')]))
|
||||
if has_keywords:
|
||||
removed_proxies.append(proxy.get("name"))
|
||||
else:
|
||||
proxies.append(proxy)
|
||||
if proxies:
|
||||
clash_config["proxies"] = proxies
|
||||
else:
|
||||
logger.warn(f"关键词过滤后无可用节点,跳过过滤")
|
||||
removed_proxies = []
|
||||
for proxy_group in clash_config.get("proxy-groups", []):
|
||||
proxy_group['proxies'] = [x for x in proxy_group.get('proxies') if x not in removed_proxies]
|
||||
clash_config["proxy-groups"] = [x for x in clash_config.get("proxy-groups", []) if x.get("proxies")]
|
||||
return clash_config
|
||||
|
||||
def clash_config(self) -> Optional[Dict[str, Any]]:
|
||||
if not self._clash_config:
|
||||
return
|
||||
self.__insert_ruleset()
|
||||
self._top_rules = self._clash_rule_parser.to_string()
|
||||
clash_config = self._clash_config.copy()
|
||||
top_rules = []
|
||||
for rule in self._clash_rule_parser.rules:
|
||||
if (not isinstance(rule.action, Action) and
|
||||
not len([x for x in self.clash_outbound(clash_config) if rule.action == x.get("name", '')])):
|
||||
logger.warn(f"出站 {rule.action} 不存在, 绕过 {rule.raw_rule}")
|
||||
continue
|
||||
top_rules.append(rule.raw_rule)
|
||||
clash_config["rules"] = self._top_rules + clash_config.get("rules", [])
|
||||
self._rule_provider = {}
|
||||
for r in self._clash_rule_parser.rules:
|
||||
if r.rule_type == RuleType.RULE_SET and r.payload.startswith(self._ruleset_prefix):
|
||||
action_str = f"{r.action.value}" if isinstance(r.action, Action) else r.action
|
||||
path_name = hashlib.sha256(action_str.encode('utf-8')).hexdigest()[:10]
|
||||
self._ruleset_names[path_name] = r.payload
|
||||
sub_url = (f"{self._movie_pilot_url}/api/v1/plugin/ClashRuleProvider/ruleset?"
|
||||
f"name={path_name}&apikey={settings.API_TOKEN}")
|
||||
self._rule_provider[r.payload] = {"behavior": "classical",
|
||||
"format": "yaml",
|
||||
"interval": 3600,
|
||||
"path": f"./CRP/{path_name}.yaml",
|
||||
"type": "http",
|
||||
"url": sub_url}
|
||||
if clash_config.get("rule-providers"):
|
||||
clash_config['rule-providers'].update(self._rule_provider)
|
||||
else:
|
||||
clash_config['rule-providers'] = self._rule_provider
|
||||
for key, item in self._ruleset_names.items():
|
||||
if item not in clash_config['rule-providers']:
|
||||
del self._ruleset_names[key]
|
||||
self.save_data('ruleset_names', self._ruleset_names)
|
||||
self.save_data('rule_provider', self._rule_provider)
|
||||
return clash_config
|
||||
486
plugins.v2/clashruleprovider/clash_rule_parser.py
Normal file
486
plugins.v2/clashruleprovider/clash_rule_parser.py
Normal file
@@ -0,0 +1,486 @@
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional, Union, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RuleType(Enum):
|
||||
"""Enumeration of all supported Clash rule types"""
|
||||
DOMAIN = "DOMAIN"
|
||||
DOMAIN_SUFFIX = "DOMAIN-SUFFIX"
|
||||
DOMAIN_KEYWORD = "DOMAIN-KEYWORD"
|
||||
DOMAIN_REGEX = "DOMAIN-REGEX"
|
||||
GEOSITE = "GEOSITE"
|
||||
|
||||
IP_CIDR = "IP-CIDR"
|
||||
IP_CIDR6 = "IP-CIDR6"
|
||||
IP_SUFFIX = "IP-SUFFIX"
|
||||
IP_ASN = "IP-ASN"
|
||||
GEOIP = "GEOIP"
|
||||
|
||||
SRC_GEOIP = "SRC-GEOIP"
|
||||
SRC_IP_ASN = "SRC-IP-ASN"
|
||||
SRC_IP_CIDR = "SRC-IP-CIDR"
|
||||
SRC_IP_SUFFIX = "SRC-IP-SUFFIX"
|
||||
|
||||
DST_PORT = "DST-PORT"
|
||||
SRC_PORT = "SRC-PORT"
|
||||
|
||||
IN_PORT = "IN-PORT"
|
||||
IN_TYPE = "IN-TYPE"
|
||||
IN_USER = "IN-USER"
|
||||
IN_NAME = "IN-NAME"
|
||||
|
||||
PROCESS_PATH = "PROCESS-PATH"
|
||||
PROCESS_PATH_REGEX = "PROCESS-PATH-REGEX"
|
||||
PROCESS_NAME = "PROCESS-NAME"
|
||||
PROCESS_NAME_REGEX = "PROCESS-NAME-REGEX"
|
||||
|
||||
UID = "UID"
|
||||
NETWORK = "NETWORK"
|
||||
DSCP = "DSCP"
|
||||
|
||||
RULE_SET = "RULE-SET"
|
||||
AND = "AND"
|
||||
OR = "OR"
|
||||
NOT = "NOT"
|
||||
SUB_RULE = "SUB-RULE"
|
||||
|
||||
MATCH = "MATCH"
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""Enumeration of rule actions"""
|
||||
DIRECT = "DIRECT"
|
||||
REJECT = "REJECT"
|
||||
REJECT_DROP = "REJECT-DROP"
|
||||
PASS = "PASS"
|
||||
COMPATIBLE = "COMPATIBLE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClashRule:
|
||||
"""Represents a parsed Clash routing rule"""
|
||||
rule_type: RuleType
|
||||
payload: str
|
||||
action: Union[Action, str] # Can be Action enum or custom proxy group name
|
||||
additional_params: Optional[List[str]] = None
|
||||
raw_rule: str = ""
|
||||
priority: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.additional_params is None:
|
||||
self.additional_params = []
|
||||
|
||||
def condition_string(self) -> str:
|
||||
return f"{self.rule_type.value},{self.payload}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LogicRule:
|
||||
"""Represents a logic rule (AND, OR, NOT)"""
|
||||
logic_type: RuleType
|
||||
conditions: List[Union[ClashRule, 'LogicRule']]
|
||||
action: Union[Action, str]
|
||||
raw_rule: str = ""
|
||||
priority: int = 0
|
||||
|
||||
def condition_string(self) -> str:
|
||||
conditions_str = ','.join([f"({c.condition_string()})" for c in self.conditions])
|
||||
return f"{self.logic_type.value},({conditions_str})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchRule:
|
||||
"""Represents a match rule"""
|
||||
action: Union[Action, str]
|
||||
raw_rule: str = ""
|
||||
priority: int = 0
|
||||
rule_type: RuleType = RuleType.MATCH
|
||||
|
||||
@staticmethod
|
||||
def condition_string() -> str:
|
||||
return "MATCH"
|
||||
|
||||
|
||||
class ClashRuleParser:
|
||||
"""Parser for Clash routing rules"""
|
||||
|
||||
def __init__(self):
|
||||
self.rules: List[Union[ClashRule, LogicRule, MatchRule]] = []
|
||||
|
||||
@staticmethod
|
||||
def parse_rule_line(line: str) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Parse a single rule line"""
|
||||
line = line.strip()
|
||||
try:
|
||||
# Handle logic rules (AND, OR, NOT)
|
||||
|
||||
if line.startswith(('AND,', 'OR,', 'NOT,')):
|
||||
return ClashRuleParser._parse_logic_rule(line)
|
||||
elif line.startswith('MATCH'):
|
||||
return ClashRuleParser._parse_match_rule(line)
|
||||
# Handle regular rules
|
||||
return ClashRuleParser._parse_regular_rule(line)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error parsing rule '{line}': {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_rule_dict(clash_rule: Dict[str, Any]) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
if not clash_rule:
|
||||
return None
|
||||
if clash_rule.get("type") in ('AND', 'OR', 'NOT'):
|
||||
conditions = clash_rule.get("conditions")
|
||||
if not conditions:
|
||||
return None
|
||||
conditions_str = ''
|
||||
for condition in conditions:
|
||||
conditions_str += f'({condition.get("type")},{condition.get("payload")})'
|
||||
conditions_str = f"({conditions_str})"
|
||||
raw_rule = f"{clash_rule.get('type')},{conditions_str},{clash_rule.get('action')}"
|
||||
return ClashRuleParser._parse_logic_rule(raw_rule)
|
||||
elif clash_rule.get("type") == 'MATCH':
|
||||
raw_rule = f"{clash_rule.get('type')},{clash_rule.get('action')}"
|
||||
return ClashRuleParser._parse_match_rule(raw_rule)
|
||||
else:
|
||||
raw_rule = f"{clash_rule.get('type')},{clash_rule.get('payload')},{clash_rule.get('action')}"
|
||||
return ClashRuleParser._parse_regular_rule(raw_rule)
|
||||
|
||||
@staticmethod
|
||||
def _parse_match_rule(line: str) -> MatchRule:
|
||||
parts = line.split(',')
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"Invalid rule format: {line}")
|
||||
action = parts[1]
|
||||
# Validate rule type
|
||||
try:
|
||||
action_enum = Action(action.upper())
|
||||
final_action = action_enum
|
||||
except ValueError:
|
||||
final_action = action
|
||||
|
||||
return MatchRule(
|
||||
action=final_action,
|
||||
raw_rule=line
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_regular_rule(line: str) -> ClashRule:
|
||||
"""Parse a regular (non-logic) rule"""
|
||||
parts = line.split(',')
|
||||
|
||||
if len(parts) < 3:
|
||||
raise ValueError(f"Invalid rule format: {line}")
|
||||
|
||||
rule_type_str = parts[0].upper()
|
||||
payload = parts[1]
|
||||
action = parts[2]
|
||||
|
||||
if not payload or not rule_type_str:
|
||||
raise ValueError(f"Invalid rule format: {line}")
|
||||
|
||||
additional_params = parts[3:] if len(parts) > 3 else []
|
||||
|
||||
# Validate rule type
|
||||
try:
|
||||
rule_type = RuleType(rule_type_str)
|
||||
except ValueError:
|
||||
raise ValueError(f"Unknown rule type: {rule_type_str}")
|
||||
|
||||
# Try to convert action to enum, otherwise keep as string (custom proxy group)
|
||||
try:
|
||||
action_enum = Action(action.upper())
|
||||
final_action = action_enum
|
||||
except ValueError:
|
||||
final_action = action
|
||||
|
||||
return ClashRule(
|
||||
rule_type=rule_type,
|
||||
payload=payload,
|
||||
action=final_action,
|
||||
additional_params=additional_params,
|
||||
raw_rule=line
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_logic_rule(line: str) -> LogicRule:
|
||||
"""Parse a logic rule (AND, OR, NOT)"""
|
||||
# Extract logic type
|
||||
logic_rule_match = re.match(r'^(AND|OR|NOT),\((.+)\),([^,]+)$', line)
|
||||
if not logic_rule_match:
|
||||
raise ValueError(f"Cannot extract action from logic rule: {line}")
|
||||
logic_type_str = logic_rule_match.group(1).upper()
|
||||
logic_type = RuleType(logic_type_str)
|
||||
action = logic_rule_match.group(3)
|
||||
# Try to convert action to enum
|
||||
try:
|
||||
action_enum = Action(action.upper())
|
||||
final_action = action_enum
|
||||
except ValueError:
|
||||
final_action = action
|
||||
conditions_str = logic_rule_match.group(2)
|
||||
conditions = ClashRuleParser._parse_logic_conditions(conditions_str)
|
||||
|
||||
return LogicRule(
|
||||
logic_type=logic_type,
|
||||
conditions=conditions,
|
||||
action=final_action,
|
||||
raw_rule=line
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_logic_conditions(conditions_str: str) -> List[ClashRule]:
|
||||
"""Parse conditions within logic rules"""
|
||||
conditions = []
|
||||
|
||||
# Simple parser for conditions like (DOMAIN,baidu.com),(NETWORK,UDP)
|
||||
# This is a basic implementation - more complex nested logic would need a proper parser
|
||||
condition_pattern = r'\(([^,]+),([^)]+)\)'
|
||||
matches = re.findall(condition_pattern, conditions_str)
|
||||
|
||||
for rule_type_str, payload in matches:
|
||||
try:
|
||||
rule_type = RuleType(rule_type_str.upper())
|
||||
condition = ClashRule(
|
||||
rule_type=rule_type,
|
||||
payload=payload,
|
||||
action="", # Logic conditions don't have actions
|
||||
raw_rule=f"{rule_type_str},{payload}"
|
||||
)
|
||||
conditions.append(condition)
|
||||
except ValueError:
|
||||
print(f"Unknown rule type in logic condition: {rule_type_str}")
|
||||
|
||||
return conditions
|
||||
|
||||
def parse_rules(self, rules_text: str) -> List[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Parse multiple rules from text, preserving order and priority"""
|
||||
self.rules = []
|
||||
lines = rules_text.strip().split('\n')
|
||||
priority = 0
|
||||
|
||||
for line in lines:
|
||||
rule = self.parse_rule_line(line)
|
||||
if rule:
|
||||
rule.priority = priority # Assign priority based on position
|
||||
self.rules.append(rule)
|
||||
priority += 1
|
||||
|
||||
return self.rules
|
||||
|
||||
def parse_rules_from_list(self, rules_list: List[str]) -> List[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Parse rules from a list of rule strings, preserving order and priority"""
|
||||
self.rules = []
|
||||
|
||||
for priority, rule_str in enumerate(rules_list):
|
||||
rule = self.parse_rule_line(rule_str)
|
||||
if rule:
|
||||
rule.priority = priority # Assign priority based on list position
|
||||
self.rules.append(rule)
|
||||
|
||||
return self.rules
|
||||
|
||||
def validate_rule(self, rule: ClashRule) -> bool:
|
||||
"""Validate a parsed rule"""
|
||||
try:
|
||||
# Basic validation based on rule type
|
||||
if rule.rule_type in [RuleType.IP_CIDR, RuleType.IP_CIDR6]:
|
||||
# Validate CIDR format
|
||||
return '/' in rule.payload
|
||||
|
||||
elif rule.rule_type == RuleType.DST_PORT or rule.rule_type == RuleType.SRC_PORT:
|
||||
# Validate port number/range
|
||||
return rule.payload.isdigit() or '-' in rule.payload
|
||||
|
||||
elif rule.rule_type == RuleType.NETWORK:
|
||||
# Validate network type
|
||||
return rule.payload.lower() in ['tcp', 'udp']
|
||||
|
||||
elif rule.rule_type == RuleType.DOMAIN_REGEX or rule.rule_type == RuleType.PROCESS_PATH_REGEX:
|
||||
# Try to compile regex
|
||||
re.compile(rule.payload)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def to_string(self) -> List[str]:
|
||||
result = []
|
||||
for rule in self.rules:
|
||||
result.append(rule.raw_rule)
|
||||
return result
|
||||
|
||||
def to_dict(self) -> List[Dict[str, Any]]:
|
||||
"""Convert parsed rules to dictionary format"""
|
||||
result = []
|
||||
|
||||
for rule in self.rules:
|
||||
if isinstance(rule, ClashRule):
|
||||
rule_dict = {
|
||||
'type': rule.rule_type.value,
|
||||
'payload': rule.payload,
|
||||
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
|
||||
'additional_params': rule.additional_params,
|
||||
'priority': rule.priority,
|
||||
'raw': rule.raw_rule
|
||||
}
|
||||
result.append(rule_dict)
|
||||
|
||||
elif isinstance(rule, LogicRule):
|
||||
conditions_dict = []
|
||||
for condition in rule.conditions:
|
||||
if isinstance(condition, ClashRule):
|
||||
conditions_dict.append({
|
||||
'type': condition.rule_type.value,
|
||||
'payload': condition.payload
|
||||
})
|
||||
|
||||
rule_dict = {
|
||||
'type': rule.logic_type.value,
|
||||
'conditions': conditions_dict,
|
||||
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
|
||||
'priority': rule.priority,
|
||||
'raw': rule.raw_rule
|
||||
}
|
||||
result.append(rule_dict)
|
||||
elif isinstance(rule, MatchRule):
|
||||
rule_dict = {
|
||||
'type': 'MATCH',
|
||||
'action': rule.action.value if isinstance(rule.action, Action) else rule.action,
|
||||
'priority': rule.priority,
|
||||
'raw': rule.raw_rule
|
||||
}
|
||||
result.append(rule_dict)
|
||||
return result
|
||||
|
||||
def get_rules_by_priority(self) -> List[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Get rules sorted by priority (highest priority first)"""
|
||||
return sorted(self.rules, key=lambda rule: rule.priority)
|
||||
|
||||
def append_rule(self, rule: Union[ClashRule, LogicRule, MatchRule]) -> None:
|
||||
max_priority = max(rule.priority for rule in self.rules) if len(self.rules) else 0
|
||||
rule.priority = max_priority + 1
|
||||
self.rules.append(rule)
|
||||
# Re-sort rules to maintain order
|
||||
self.rules.sort(key=lambda r: r.priority)
|
||||
|
||||
def insert_rule_at_priority(self, rule: Union[ClashRule, LogicRule, MatchRule], priority: int):
|
||||
"""Insert a rule at a specific priority position, adjusting other rules"""
|
||||
# Adjust priorities of existing rules
|
||||
for existing_rule in self.rules:
|
||||
if existing_rule.priority >= priority:
|
||||
existing_rule.priority += 1
|
||||
rule.priority = priority
|
||||
self.rules.append(rule)
|
||||
|
||||
# Re-sort rules to maintain order
|
||||
self.rules.sort(key=lambda r: r.priority)
|
||||
|
||||
def update_rule_at_priority(self, clash_rule: Union[ClashRule, LogicRule], priority: int) -> bool:
|
||||
for index, existing_rule in enumerate(self.rules):
|
||||
if existing_rule.priority == priority:
|
||||
self.rules[index] = clash_rule
|
||||
self.rules[index].priority = priority
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_rule_at_priority(self, priority: int) -> Optional[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Remove rule at specific priority and adjust remaining priorities"""
|
||||
rule_to_remove = None
|
||||
for rule in self.rules:
|
||||
if rule.priority == priority:
|
||||
rule_to_remove = rule
|
||||
break
|
||||
|
||||
if rule_to_remove:
|
||||
self.rules.remove(rule_to_remove)
|
||||
|
||||
# Adjust priorities of remaining rules
|
||||
for rule in self.rules:
|
||||
if rule.priority > priority:
|
||||
rule.priority -= 1
|
||||
|
||||
return rule_to_remove
|
||||
return None
|
||||
|
||||
def remove_rules(self, condition: Callable[[Union[ClashRule, LogicRule, MatchRule]], bool]):
|
||||
"""Remove rules by lambda"""
|
||||
i = 0
|
||||
while i < len(self.rules):
|
||||
if condition(self.rules[i]):
|
||||
priority = self.rules[i].priority
|
||||
for rule in self.rules:
|
||||
if rule.priority > priority:
|
||||
rule.priority -= 1
|
||||
del self.rules[i]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
def move_rule_priority(self, from_priority: int, to_priority: int) -> bool:
|
||||
"""Move a rule from one priority position to another"""
|
||||
rule_to_move = None
|
||||
for rule in self.rules:
|
||||
if rule.priority == from_priority:
|
||||
rule_to_move = rule
|
||||
break
|
||||
|
||||
if not rule_to_move:
|
||||
return False
|
||||
|
||||
# Remove rule temporarily
|
||||
self.remove_rule_at_priority(from_priority)
|
||||
|
||||
# Insert at new priority
|
||||
self.insert_rule_at_priority(rule_to_move, to_priority)
|
||||
|
||||
return True
|
||||
|
||||
def filter_rules_by_type(self, rule_type: RuleType) -> List[ClashRule]:
|
||||
"""Filter rules by type"""
|
||||
return [rule for rule in self.rules
|
||||
if isinstance(rule, ClashRule) and rule.rule_type == rule_type]
|
||||
|
||||
def filter_rules_by_action(self, action: Union[Action, str]) -> List[Union[ClashRule, LogicRule, MatchRule]]:
|
||||
"""Filter rules by action"""
|
||||
return [rule for rule in self.rules if rule.action == action]
|
||||
|
||||
def has_rule(self, clash_rule: Union[ClashRule, LogicRule, MatchRule]) -> bool:
|
||||
for rule in self.rules:
|
||||
if rule.rule_type == clash_rule.rule_type and rule.action == clash_rule.action \
|
||||
and rule.payload == clash_rule.payload:
|
||||
return True
|
||||
return False
|
||||
|
||||
def reorder_rules(
|
||||
self,
|
||||
moved_rule_priority: int,
|
||||
target_priority: int,
|
||||
):
|
||||
"""
|
||||
Reorder rules
|
||||
|
||||
:param moved_rule_priority: 被移动规则的原始优先级
|
||||
:param target_priority: 目标位置的优先级
|
||||
"""
|
||||
moved_index = next(i for i, r in enumerate(self.rules) if r.priority == moved_rule_priority)
|
||||
target_index = next(
|
||||
(i for i, r in enumerate(self.rules) if r.priority == target_priority),
|
||||
len(self.rules)
|
||||
)
|
||||
# 直接修改被移动规则的优先级
|
||||
moved_rule = self.rules[moved_index]
|
||||
moved_rule.priority = target_priority
|
||||
|
||||
if moved_index < target_index:
|
||||
# 向后移动:原位置到目标位置之间的规则优先级 -1
|
||||
for i in range(moved_index + 1, target_index + 1):
|
||||
self.rules[i].priority -= 1
|
||||
elif moved_index > target_index:
|
||||
# 向前移动:目标位置到原位置之间的规则优先级 +1
|
||||
for i in range(target_index, moved_index):
|
||||
self.rules[i].priority += 1
|
||||
self.rules.sort(key=lambda x: x.priority)
|
||||
828
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-C3BpNVeC.js
vendored
Normal file
828
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-C3BpNVeC.js
vendored
Normal file
@@ -0,0 +1,828 @@
|
||||
import { importShared } from './__federation_fn_import-JrT3xvdd.js';
|
||||
import { _ as _export_sfc } from './_plugin-vue_export-helper-pcqpp-6-.js';
|
||||
|
||||
const {createTextVNode:_createTextVNode,resolveComponent:_resolveComponent,withCtx:_withCtx,createVNode:_createVNode,toDisplayString:_toDisplayString,openBlock:_openBlock,createBlock:_createBlock,createCommentVNode:_createCommentVNode,createElementVNode:_createElementVNode,mergeProps:_mergeProps,withModifiers:_withModifiers,createElementBlock:_createElementBlock} = await importShared('vue');
|
||||
|
||||
|
||||
const _hoisted_1 = { class: "plugin-config" };
|
||||
const _hoisted_2 = { class: "d-flex align-center" };
|
||||
const _hoisted_3 = { class: "font-weight-medium" };
|
||||
const _hoisted_4 = { class: "text-body-2" };
|
||||
|
||||
const {ref,reactive,onMounted,computed} = await importShared('vue');
|
||||
|
||||
|
||||
// Props
|
||||
|
||||
const _sfc_main = {
|
||||
__name: 'Config',
|
||||
props: {
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['save', 'close'],
|
||||
setup(__props, { emit: __emit }) {
|
||||
|
||||
const props = __props;
|
||||
|
||||
// 状态变量
|
||||
const form = ref(null);
|
||||
const isFormValid = ref(true);
|
||||
const error = ref(null);
|
||||
const saving = ref(false);
|
||||
const testing = ref(false);
|
||||
const showClashSecret = ref(false);
|
||||
const selectedCronOption = ref('6hours');
|
||||
|
||||
// Test result state
|
||||
const testResult = reactive({
|
||||
show: false,
|
||||
success: false,
|
||||
title: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
// Cron 选项
|
||||
const cronOptions = [
|
||||
{text: '每5分钟', value: '5min', cron: '*/5 * * * *'},
|
||||
{text: '每15分钟', value: '15min', cron: '*/15 * * * *'},
|
||||
{text: '每30分钟', value: '30min', cron: '*/30 * * * *'},
|
||||
{text: '每小时', value: '1hour', cron: '0 * * * *'},
|
||||
{text: '每2小时', value: '2hours', cron: '0 */2 * * *'},
|
||||
{text: '每6小时', value: '6hours', cron: '0 */6 * * *'},
|
||||
{text: '每12小时', value: '12hours', cron: '0 */12 * * *'},
|
||||
{text: '每天', value: '1day', cron: '0 0 * * *'},
|
||||
{text: '自定义', value: 'custom', cron: ''},
|
||||
];
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig = {
|
||||
enabled: false,
|
||||
sub_links: [],
|
||||
filter_keywords: ["公益性", "高延迟", "域名", "官网", "重启", "过期时间", "系统代理"],
|
||||
clash_dashboard_url: '',
|
||||
clash_dashboard_secret: '',
|
||||
movie_pilot_url: '',
|
||||
cron_string: '0 */6 * * *',
|
||||
timeout: 10,
|
||||
retry_times: 3,
|
||||
proxy: false,
|
||||
notify: false,
|
||||
auto_update_subscriptions: true,
|
||||
ruleset_prefix: '📂<-',
|
||||
};
|
||||
|
||||
// 响应式配置对象
|
||||
const config = reactive({...defaultConfig});
|
||||
|
||||
// 自定义事件
|
||||
const emit = __emit;
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.initialConfig) {
|
||||
Object.keys(props.initialConfig).forEach(key => {
|
||||
if (key in config) {
|
||||
config[key] = props.initialConfig[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 设置对应的cron选项
|
||||
const cronOption = cronOptions.find(option => option.cron === config.cron_string);
|
||||
if (cronOption) {
|
||||
selectedCronOption.value = cronOption.value;
|
||||
} else {
|
||||
selectedCronOption.value = 'custom';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 验证函数
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function validateSubLinks(links) {
|
||||
if (!links || links.length === 0) {
|
||||
return '至少需要一个订阅链接'
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if (!isValidUrl(link)) {
|
||||
return `无效的订阅链接: ${link}`
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function validateCronExpression(cronStr) {
|
||||
if (!cronStr) return '请输入Cron表达式'
|
||||
|
||||
// 简单的cron表达式验证
|
||||
const parts = cronStr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
return 'Cron表达式应包含5个部分 (分 时 日 月 周)'
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 更新cron字符串
|
||||
function updateCronString(optionValue) {
|
||||
const option = cronOptions.find(opt => opt.value === optionValue);
|
||||
if (option && option.cron) {
|
||||
config.cron_string = option.cron;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
async function testConnection() {
|
||||
testing.value = true;
|
||||
error.value = null;
|
||||
testResult.show = false;
|
||||
|
||||
try {
|
||||
// 验证必需的参数
|
||||
if (!config.clash_dashboard_url) {
|
||||
throw new Error('请先配置 Clash 面板 URL')
|
||||
}
|
||||
if (!config.clash_dashboard_secret) {
|
||||
throw new Error('请先配置 Clash 面板密钥')
|
||||
}
|
||||
if (!config.sub_links || config.sub_links.length === 0) {
|
||||
throw new Error('请先配置至少一个订阅链接')
|
||||
}
|
||||
if (!config.movie_pilot_url || config.movie_pilot_url.length === 0) {
|
||||
throw new Error('请先MoviePilot链接')
|
||||
}
|
||||
// 准备API请求参数
|
||||
const testParams = {
|
||||
clash_dashboard_url: config.clash_dashboard_url,
|
||||
clash_dashboard_secret: config.clash_dashboard_secret,
|
||||
sub_link: config.sub_links[0] // 使用第一个订阅链接进行测试
|
||||
};
|
||||
|
||||
// 调用API进行连接测试
|
||||
const result = await props.api.post('/plugin/ClashRuleProvider/connectivity', testParams);
|
||||
|
||||
// 根据返回结果显示相应消息
|
||||
if (result.success) {
|
||||
testResult.success = true;
|
||||
testResult.title = '连接测试成功!';
|
||||
testResult.message = 'Clash面板和订阅链接连接正常,配置验证通过';
|
||||
testResult.show = true;
|
||||
|
||||
// Auto hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
testResult.show = false;
|
||||
}, 5000);
|
||||
} else {
|
||||
throw new Error(result.message || '连接测试失败,请检查配置')
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('连接测试失败:', err);
|
||||
testResult.success = false;
|
||||
testResult.title = '连接测试失败';
|
||||
testResult.message = err.message;
|
||||
testResult.show = true;
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
if (!isFormValid.value) {
|
||||
error.value = '请修正表单中的错误';
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
emit('save', {...config});
|
||||
} catch (err) {
|
||||
console.error('保存配置失败:', err);
|
||||
error.value = err.message || '保存配置失败';
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractDomain(url) {
|
||||
try {
|
||||
const domain = new URL(url).hostname;
|
||||
return domain.startsWith('www.') ? domain.substring(4) : domain
|
||||
} catch {
|
||||
return url // 如果解析失败,返回原始URL
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
Object.keys(defaultConfig).forEach(key => {
|
||||
config[key] = defaultConfig[key];
|
||||
});
|
||||
selectedCronOption.value = '6hours';
|
||||
|
||||
if (form.value) {
|
||||
form.value.resetValidation();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭组件
|
||||
function notifyClose() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// 通知主应用切换到Page页面
|
||||
function notifySwitch() {
|
||||
emit('switch');
|
||||
}
|
||||
|
||||
return (_ctx, _cache) => {
|
||||
const _component_v_card_title = _resolveComponent("v-card-title");
|
||||
const _component_v_icon = _resolveComponent("v-icon");
|
||||
const _component_v_btn = _resolveComponent("v-btn");
|
||||
const _component_v_card_item = _resolveComponent("v-card-item");
|
||||
const _component_v_alert = _resolveComponent("v-alert");
|
||||
const _component_v_switch = _resolveComponent("v-switch");
|
||||
const _component_v_col = _resolveComponent("v-col");
|
||||
const _component_v_row = _resolveComponent("v-row");
|
||||
const _component_v_chip = _resolveComponent("v-chip");
|
||||
const _component_v_combobox = _resolveComponent("v-combobox");
|
||||
const _component_v_text_field = _resolveComponent("v-text-field");
|
||||
const _component_v_select = _resolveComponent("v-select");
|
||||
const _component_v_expansion_panel_title = _resolveComponent("v-expansion-panel-title");
|
||||
const _component_v_expansion_panel_text = _resolveComponent("v-expansion-panel-text");
|
||||
const _component_v_expansion_panel = _resolveComponent("v-expansion-panel");
|
||||
const _component_v_expansion_panels = _resolveComponent("v-expansion-panels");
|
||||
const _component_v_form = _resolveComponent("v-form");
|
||||
const _component_v_card_text = _resolveComponent("v-card-text");
|
||||
const _component_v_spacer = _resolveComponent("v-spacer");
|
||||
const _component_v_card_actions = _resolveComponent("v-card-actions");
|
||||
const _component_v_card = _resolveComponent("v-card");
|
||||
|
||||
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
|
||||
_createVNode(_component_v_card, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_card_item, null, {
|
||||
append: _withCtx(() => [
|
||||
_createVNode(_component_v_btn, {
|
||||
icon: "",
|
||||
color: "primary",
|
||||
variant: "text",
|
||||
onClick: notifyClose
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { left: "" }, {
|
||||
default: _withCtx(() => _cache[18] || (_cache[18] = [
|
||||
_createTextVNode("mdi-close")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_card_title, null, {
|
||||
default: _withCtx(() => _cache[17] || (_cache[17] = [
|
||||
_createTextVNode("Clash Rule Provider 插件配置")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_card_text, { class: "overflow-y-auto" }, {
|
||||
default: _withCtx(() => [
|
||||
(error.value)
|
||||
? (_openBlock(), _createBlock(_component_v_alert, {
|
||||
key: 0,
|
||||
type: "error",
|
||||
class: "mb-4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(error.value), 1)
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
: _createCommentVNode("", true),
|
||||
_createVNode(_component_v_form, {
|
||||
ref_key: "form",
|
||||
ref: form,
|
||||
modelValue: isFormValid.value,
|
||||
"onUpdate:modelValue": _cache[15] || (_cache[15] = $event => ((isFormValid).value = $event)),
|
||||
onSubmit: _withModifiers(saveConfig, ["prevent"])
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_cache[28] || (_cache[28] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "基本设置", -1)),
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.enabled,
|
||||
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((config.enabled) = $event)),
|
||||
label: "启用插件",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "启用后插件将开始监控和同步",
|
||||
"persistent-hint": ""
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.proxy,
|
||||
"onUpdate:modelValue": _cache[1] || (_cache[1] = $event => ((config.proxy) = $event)),
|
||||
label: "启用代理",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "是否使用系统代理进行网络请求",
|
||||
"persistent-hint": ""
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.notify,
|
||||
"onUpdate:modelValue": _cache[2] || (_cache[2] = $event => ((config.notify) = $event)),
|
||||
label: "启用通知",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "执行完成后发送通知消息",
|
||||
"persistent-hint": ""
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[29] || (_cache[29] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "订阅配置", -1)),
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_combobox, {
|
||||
modelValue: config.sub_links,
|
||||
"onUpdate:modelValue": _cache[3] || (_cache[3] = $event => ((config.sub_links) = $event)),
|
||||
label: "订阅链接",
|
||||
variant: "outlined",
|
||||
multiple: "",
|
||||
chips: "",
|
||||
"closable-chips": "",
|
||||
hint: "添加一个Clash订阅链接",
|
||||
"persistent-hint": "",
|
||||
rules: [validateSubLinks]
|
||||
}, {
|
||||
chip: _withCtx(({ props, item }) => [
|
||||
_createVNode(_component_v_chip, _mergeProps(props, {
|
||||
closable: "",
|
||||
size: "small"
|
||||
}), {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(extractDomain(item.value)), 1)
|
||||
]),
|
||||
_: 2
|
||||
}, 1040)
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_combobox, {
|
||||
modelValue: config.filter_keywords,
|
||||
"onUpdate:modelValue": _cache[4] || (_cache[4] = $event => ((config.filter_keywords) = $event)),
|
||||
label: "节点过滤关键词",
|
||||
variant: "outlined",
|
||||
multiple: "",
|
||||
chips: "",
|
||||
"closable-chips": "",
|
||||
hint: "添加用于过滤节点的关键词",
|
||||
"persistent-hint": ""
|
||||
}, {
|
||||
chip: _withCtx(({ props, item }) => [
|
||||
_createVNode(_component_v_chip, _mergeProps(props, {
|
||||
closable: "",
|
||||
size: "small",
|
||||
color: "info"
|
||||
}), {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(item.value), 1)
|
||||
]),
|
||||
_: 2
|
||||
}, 1040)
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[30] || (_cache[30] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "Clash 面板设置", -1)),
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.clash_dashboard_url,
|
||||
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => ((config.clash_dashboard_url) = $event)),
|
||||
label: "Clash 面板 URL",
|
||||
variant: "outlined",
|
||||
placeholder: "http://localhost:9090",
|
||||
hint: "Clash 控制面板的访问地址",
|
||||
"persistent-hint": "",
|
||||
rules: [v => !v || isValidUrl(v) || '请输入有效的URL地址']
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "primary" }, {
|
||||
default: _withCtx(() => _cache[19] || (_cache[19] = [
|
||||
_createTextVNode("mdi-web")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.clash_dashboard_secret,
|
||||
"onUpdate:modelValue": _cache[6] || (_cache[6] = $event => ((config.clash_dashboard_secret) = $event)),
|
||||
label: "Clash 面板密钥",
|
||||
variant: "outlined",
|
||||
placeholder: "your-clash-secret",
|
||||
hint: "用于访问Clash API的密钥",
|
||||
"persistent-hint": "",
|
||||
"append-inner-icon": showClashSecret.value ? 'mdi-eye-off' : 'mdi-eye',
|
||||
type: showClashSecret.value ? 'text' : 'password',
|
||||
"onClick:appendInner": _cache[7] || (_cache[7] = $event => (showClashSecret.value = !showClashSecret.value))
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "warning" }, {
|
||||
default: _withCtx(() => _cache[20] || (_cache[20] = [
|
||||
_createTextVNode("mdi-key")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "append-inner-icon", "type"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[31] || (_cache[31] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "MoviePilot 设置", -1)),
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.movie_pilot_url,
|
||||
"onUpdate:modelValue": _cache[8] || (_cache[8] = $event => ((config.movie_pilot_url) = $event)),
|
||||
label: "MoviePilot URL",
|
||||
variant: "outlined",
|
||||
placeholder: "http://localhost:3001",
|
||||
hint: "MoviePilot 服务的访问地址",
|
||||
"persistent-hint": "",
|
||||
rules: [v => !!v || 'MoviePilot URL不能为空', v => isValidUrl(v) || '请输入有效的URL地址']
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "success" }, {
|
||||
default: _withCtx(() => _cache[21] || (_cache[21] = [
|
||||
_createTextVNode("mdi-movie")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_cache[32] || (_cache[32] = _createElementVNode("div", { class: "text-subtitle-1 font-weight-bold mt-4 mb-2" }, "执行设置", -1)),
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_select, {
|
||||
modelValue: selectedCronOption.value,
|
||||
"onUpdate:modelValue": [
|
||||
_cache[9] || (_cache[9] = $event => ((selectedCronOption).value = $event)),
|
||||
updateCronString
|
||||
],
|
||||
label: "执行周期",
|
||||
items: cronOptions,
|
||||
variant: "outlined",
|
||||
"item-title": "text",
|
||||
"item-value": "value",
|
||||
hint: "选择插件执行的时间间隔",
|
||||
"persistent-hint": ""
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
(selectedCronOption.value === 'custom')
|
||||
? (_openBlock(), _createBlock(_component_v_col, {
|
||||
key: 0,
|
||||
cols: "12"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.cron_string,
|
||||
"onUpdate:modelValue": _cache[10] || (_cache[10] = $event => ((config.cron_string) = $event)),
|
||||
label: "自定义 Cron 表达式",
|
||||
variant: "outlined",
|
||||
placeholder: "0 */6 * * *",
|
||||
hint: "使用标准Cron表达式格式 (分 时 日 月 周)",
|
||||
"persistent-hint": "",
|
||||
rules: [validateCronExpression]
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[22] || (_cache[22] = [
|
||||
_createTextVNode("mdi-clock-outline")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
}))
|
||||
: _createCommentVNode("", true),
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.timeout,
|
||||
"onUpdate:modelValue": _cache[11] || (_cache[11] = $event => ((config.timeout) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "超时时间 (秒)",
|
||||
variant: "outlined",
|
||||
type: "number",
|
||||
min: "1",
|
||||
max: "300",
|
||||
hint: "请求的超时时间",
|
||||
"persistent-hint": "",
|
||||
rules: [v => v > 0 || '超时时间必须大于0']
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "warning" }, {
|
||||
default: _withCtx(() => _cache[23] || (_cache[23] = [
|
||||
_createTextVNode("mdi-timer")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, {
|
||||
cols: "12",
|
||||
md: "6"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.retry_times,
|
||||
"onUpdate:modelValue": _cache[12] || (_cache[12] = $event => ((config.retry_times) = $event)),
|
||||
modelModifiers: { number: true },
|
||||
label: "重试次数",
|
||||
variant: "outlined",
|
||||
type: "number",
|
||||
min: "0",
|
||||
max: "10",
|
||||
hint: "失败时的重试次数",
|
||||
"persistent-hint": "",
|
||||
rules: [v => v >= 0 || '重试次数不能为负数']
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[24] || (_cache[24] = [
|
||||
_createTextVNode("mdi-refresh")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue", "rules"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_expansion_panels, {
|
||||
variant: "accordion",
|
||||
class: "mt-4"
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_expansion_panel, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_expansion_panel_title, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { class: "mr-2" }, {
|
||||
default: _withCtx(() => _cache[25] || (_cache[25] = [
|
||||
_createTextVNode("mdi-cog")
|
||||
])),
|
||||
_: 1
|
||||
}),
|
||||
_cache[26] || (_cache[26] = _createTextVNode(" 高级选项 "))
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_expansion_panel_text, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_row, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_switch, {
|
||||
modelValue: config.auto_update_subscriptions,
|
||||
"onUpdate:modelValue": _cache[13] || (_cache[13] = $event => ((config.auto_update_subscriptions) = $event)),
|
||||
label: "自动更新订阅",
|
||||
color: "primary",
|
||||
inset: "",
|
||||
hint: "定期自动更新Clash订阅配置"
|
||||
}, null, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_col, { cols: "12" }, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_text_field, {
|
||||
modelValue: config.ruleset_prefix,
|
||||
"onUpdate:modelValue": _cache[14] || (_cache[14] = $event => ((config.ruleset_prefix) = $event)),
|
||||
label: "规则集前缀",
|
||||
variant: "outlined",
|
||||
placeholder: "📂<-",
|
||||
hint: "为生成的规则集添加前缀",
|
||||
"persistent-hint": ""
|
||||
}, {
|
||||
"prepend-inner": _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { color: "info" }, {
|
||||
default: _withCtx(() => _cache[27] || (_cache[27] = [
|
||||
_createTextVNode("mdi-prefix")
|
||||
])),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["modelValue"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_card_actions, null, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_btn, {
|
||||
color: "primary",
|
||||
onClick: notifySwitch
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createVNode(_component_v_icon, { left: "" }, {
|
||||
default: _withCtx(() => _cache[33] || (_cache[33] = [
|
||||
_createTextVNode("mdi-view-dashboard-edit")
|
||||
])),
|
||||
_: 1
|
||||
}),
|
||||
_cache[34] || (_cache[34] = _createTextVNode(" 规则 "))
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_btn, {
|
||||
color: "secondary",
|
||||
onClick: resetForm
|
||||
}, {
|
||||
default: _withCtx(() => _cache[35] || (_cache[35] = [
|
||||
_createTextVNode("重置")
|
||||
])),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode(_component_v_btn, {
|
||||
color: "info",
|
||||
onClick: testConnection,
|
||||
loading: testing.value
|
||||
}, {
|
||||
default: _withCtx(() => _cache[36] || (_cache[36] = [
|
||||
_createTextVNode("测试连接")
|
||||
])),
|
||||
_: 1
|
||||
}, 8, ["loading"]),
|
||||
_createVNode(_component_v_spacer),
|
||||
_createVNode(_component_v_btn, {
|
||||
color: "primary",
|
||||
disabled: !isFormValid.value,
|
||||
onClick: saveConfig,
|
||||
loading: saving.value
|
||||
}, {
|
||||
default: _withCtx(() => _cache[37] || (_cache[37] = [
|
||||
_createTextVNode(" 保存配置 ")
|
||||
])),
|
||||
_: 1
|
||||
}, 8, ["disabled", "loading"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
(testResult.show)
|
||||
? (_openBlock(), _createBlock(_component_v_alert, {
|
||||
key: 0,
|
||||
type: testResult.success ? 'success' : 'error',
|
||||
variant: "tonal",
|
||||
closable: "",
|
||||
class: "ma-4 mt-0",
|
||||
"onClick:close": _cache[16] || (_cache[16] = $event => (testResult.show = false))
|
||||
}, {
|
||||
default: _withCtx(() => [
|
||||
_createElementVNode("div", _hoisted_2, [
|
||||
_createVNode(_component_v_icon, { class: "mr-2" }, {
|
||||
default: _withCtx(() => [
|
||||
_createTextVNode(_toDisplayString(testResult.success ? 'mdi-check-circle' : 'mdi-alert-circle'), 1)
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createElementVNode("div", null, [
|
||||
_createElementVNode("div", _hoisted_3, _toDisplayString(testResult.title), 1),
|
||||
_createElementVNode("div", _hoisted_4, _toDisplayString(testResult.message), 1)
|
||||
])
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
}, 8, ["type"]))
|
||||
: _createCommentVNode("", true)
|
||||
]),
|
||||
_: 1
|
||||
})
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const ConfigComponent = /*#__PURE__*/_export_sfc(_sfc_main, [['__scopeId',"data-v-0e64dae0"]]);
|
||||
|
||||
export { ConfigComponent as default };
|
||||
5
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-DXzIavcD.css
vendored
Normal file
5
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Config-DXzIavcD.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
.plugin-config[data-v-0e64dae0] {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
40514
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-BkyO-3pr.js
vendored
Normal file
40514
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Dashboard-BkyO-3pr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
48
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-Bl7XNZ7k.css
vendored
Normal file
48
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-Bl7XNZ7k.css
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
.plugin-page[data-v-d5e502a5] {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 使卡片等宽并适应移动端 */
|
||||
.d-flex.flex-wrap[data-v-d5e502a5] {
|
||||
gap: 16px;
|
||||
}
|
||||
.url-display[data-v-d5e502a5] {
|
||||
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-d5e502a5] {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add visual distinction between sections */
|
||||
.ruleset-section[data-v-d5e502a5] {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.top-section[data-v-d5e502a5] {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* Optional: Add different border colors to further distinguish */
|
||||
.ruleset-section[data-v-d5e502a5] {
|
||||
border-left: 4px solid #2196F3; /* Blue accent */
|
||||
}
|
||||
.top-section[data-v-d5e502a5] {
|
||||
border-left: 4px solid #4CAF50; /* Green accent */
|
||||
}
|
||||
.drag-handle[data-v-d5e502a5] {
|
||||
cursor: move;
|
||||
}
|
||||
1118
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DlQgf7u6.js
vendored
Normal file
1118
plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DlQgf7u6.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
418
plugins.v2/clashruleprovider/dist/assets/__federation_fn_import-JrT3xvdd.js
vendored
Normal file
418
plugins.v2/clashruleprovider/dist/assets/__federation_fn_import-JrT3xvdd.js
vendored
Normal file
@@ -0,0 +1,418 @@
|
||||
const buildIdentifier = "[0-9A-Za-z-]+";
|
||||
const build = `(?:\\+(${buildIdentifier}(?:\\.${buildIdentifier})*))`;
|
||||
const numericIdentifier = "0|[1-9]\\d*";
|
||||
const numericIdentifierLoose = "[0-9]+";
|
||||
const nonNumericIdentifier = "\\d*[a-zA-Z-][a-zA-Z0-9-]*";
|
||||
const preReleaseIdentifierLoose = `(?:${numericIdentifierLoose}|${nonNumericIdentifier})`;
|
||||
const preReleaseLoose = `(?:-?(${preReleaseIdentifierLoose}(?:\\.${preReleaseIdentifierLoose})*))`;
|
||||
const preReleaseIdentifier = `(?:${numericIdentifier}|${nonNumericIdentifier})`;
|
||||
const preRelease = `(?:-(${preReleaseIdentifier}(?:\\.${preReleaseIdentifier})*))`;
|
||||
const xRangeIdentifier = `${numericIdentifier}|x|X|\\*`;
|
||||
const xRangePlain = `[v=\\s]*(${xRangeIdentifier})(?:\\.(${xRangeIdentifier})(?:\\.(${xRangeIdentifier})(?:${preRelease})?${build}?)?)?`;
|
||||
const hyphenRange = `^\\s*(${xRangePlain})\\s+-\\s+(${xRangePlain})\\s*$`;
|
||||
const mainVersionLoose = `(${numericIdentifierLoose})\\.(${numericIdentifierLoose})\\.(${numericIdentifierLoose})`;
|
||||
const loosePlain = `[v=\\s]*${mainVersionLoose}${preReleaseLoose}?${build}?`;
|
||||
const gtlt = "((?:<|>)?=?)";
|
||||
const comparatorTrim = `(\\s*)${gtlt}\\s*(${loosePlain}|${xRangePlain})`;
|
||||
const loneTilde = "(?:~>?)";
|
||||
const tildeTrim = `(\\s*)${loneTilde}\\s+`;
|
||||
const loneCaret = "(?:\\^)";
|
||||
const caretTrim = `(\\s*)${loneCaret}\\s+`;
|
||||
const star = "(<|>)?=?\\s*\\*";
|
||||
const caret = `^${loneCaret}${xRangePlain}$`;
|
||||
const mainVersion = `(${numericIdentifier})\\.(${numericIdentifier})\\.(${numericIdentifier})`;
|
||||
const fullPlain = `v?${mainVersion}${preRelease}?${build}?`;
|
||||
const tilde = `^${loneTilde}${xRangePlain}$`;
|
||||
const xRange = `^${gtlt}\\s*${xRangePlain}$`;
|
||||
const comparator = `^${gtlt}\\s*(${fullPlain})$|^$`;
|
||||
const gte0 = "^\\s*>=\\s*0.0.0\\s*$";
|
||||
function parseRegex(source) {
|
||||
return new RegExp(source);
|
||||
}
|
||||
function isXVersion(version) {
|
||||
return !version || version.toLowerCase() === "x" || version === "*";
|
||||
}
|
||||
function pipe(...fns) {
|
||||
return (x) => {
|
||||
return fns.reduce((v, f) => f(v), x);
|
||||
};
|
||||
}
|
||||
function extractComparator(comparatorString) {
|
||||
return comparatorString.match(parseRegex(comparator));
|
||||
}
|
||||
function combineVersion(major, minor, patch, preRelease2) {
|
||||
const mainVersion2 = `${major}.${minor}.${patch}`;
|
||||
if (preRelease2) {
|
||||
return `${mainVersion2}-${preRelease2}`;
|
||||
}
|
||||
return mainVersion2;
|
||||
}
|
||||
function parseHyphen(range) {
|
||||
return range.replace(
|
||||
parseRegex(hyphenRange),
|
||||
(_range, from, fromMajor, fromMinor, fromPatch, _fromPreRelease, _fromBuild, to, toMajor, toMinor, toPatch, toPreRelease) => {
|
||||
if (isXVersion(fromMajor)) {
|
||||
from = "";
|
||||
} else if (isXVersion(fromMinor)) {
|
||||
from = `>=${fromMajor}.0.0`;
|
||||
} else if (isXVersion(fromPatch)) {
|
||||
from = `>=${fromMajor}.${fromMinor}.0`;
|
||||
} else {
|
||||
from = `>=${from}`;
|
||||
}
|
||||
if (isXVersion(toMajor)) {
|
||||
to = "";
|
||||
} else if (isXVersion(toMinor)) {
|
||||
to = `<${+toMajor + 1}.0.0-0`;
|
||||
} else if (isXVersion(toPatch)) {
|
||||
to = `<${toMajor}.${+toMinor + 1}.0-0`;
|
||||
} else if (toPreRelease) {
|
||||
to = `<=${toMajor}.${toMinor}.${toPatch}-${toPreRelease}`;
|
||||
} else {
|
||||
to = `<=${to}`;
|
||||
}
|
||||
return `${from} ${to}`.trim();
|
||||
}
|
||||
);
|
||||
}
|
||||
function parseComparatorTrim(range) {
|
||||
return range.replace(parseRegex(comparatorTrim), "$1$2$3");
|
||||
}
|
||||
function parseTildeTrim(range) {
|
||||
return range.replace(parseRegex(tildeTrim), "$1~");
|
||||
}
|
||||
function parseCaretTrim(range) {
|
||||
return range.replace(parseRegex(caretTrim), "$1^");
|
||||
}
|
||||
function parseCarets(range) {
|
||||
return range.trim().split(/\s+/).map((rangeVersion) => {
|
||||
return rangeVersion.replace(
|
||||
parseRegex(caret),
|
||||
(_, major, minor, patch, preRelease2) => {
|
||||
if (isXVersion(major)) {
|
||||
return "";
|
||||
} else if (isXVersion(minor)) {
|
||||
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
|
||||
} else if (isXVersion(patch)) {
|
||||
if (major === "0") {
|
||||
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
|
||||
} else {
|
||||
return `>=${major}.${minor}.0 <${+major + 1}.0.0-0`;
|
||||
}
|
||||
} else if (preRelease2) {
|
||||
if (major === "0") {
|
||||
if (minor === "0") {
|
||||
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${minor}.${+patch + 1}-0`;
|
||||
} else {
|
||||
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
|
||||
}
|
||||
} else {
|
||||
return `>=${major}.${minor}.${patch}-${preRelease2} <${+major + 1}.0.0-0`;
|
||||
}
|
||||
} else {
|
||||
if (major === "0") {
|
||||
if (minor === "0") {
|
||||
return `>=${major}.${minor}.${patch} <${major}.${minor}.${+patch + 1}-0`;
|
||||
} else {
|
||||
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
|
||||
}
|
||||
}
|
||||
return `>=${major}.${minor}.${patch} <${+major + 1}.0.0-0`;
|
||||
}
|
||||
}
|
||||
);
|
||||
}).join(" ");
|
||||
}
|
||||
function parseTildes(range) {
|
||||
return range.trim().split(/\s+/).map((rangeVersion) => {
|
||||
return rangeVersion.replace(
|
||||
parseRegex(tilde),
|
||||
(_, major, minor, patch, preRelease2) => {
|
||||
if (isXVersion(major)) {
|
||||
return "";
|
||||
} else if (isXVersion(minor)) {
|
||||
return `>=${major}.0.0 <${+major + 1}.0.0-0`;
|
||||
} else if (isXVersion(patch)) {
|
||||
return `>=${major}.${minor}.0 <${major}.${+minor + 1}.0-0`;
|
||||
} else if (preRelease2) {
|
||||
return `>=${major}.${minor}.${patch}-${preRelease2} <${major}.${+minor + 1}.0-0`;
|
||||
}
|
||||
return `>=${major}.${minor}.${patch} <${major}.${+minor + 1}.0-0`;
|
||||
}
|
||||
);
|
||||
}).join(" ");
|
||||
}
|
||||
function parseXRanges(range) {
|
||||
return range.split(/\s+/).map((rangeVersion) => {
|
||||
return rangeVersion.trim().replace(
|
||||
parseRegex(xRange),
|
||||
(ret, gtlt2, major, minor, patch, preRelease2) => {
|
||||
const isXMajor = isXVersion(major);
|
||||
const isXMinor = isXMajor || isXVersion(minor);
|
||||
const isXPatch = isXMinor || isXVersion(patch);
|
||||
if (gtlt2 === "=" && isXPatch) {
|
||||
gtlt2 = "";
|
||||
}
|
||||
preRelease2 = "";
|
||||
if (isXMajor) {
|
||||
if (gtlt2 === ">" || gtlt2 === "<") {
|
||||
return "<0.0.0-0";
|
||||
} else {
|
||||
return "*";
|
||||
}
|
||||
} else if (gtlt2 && isXPatch) {
|
||||
if (isXMinor) {
|
||||
minor = 0;
|
||||
}
|
||||
patch = 0;
|
||||
if (gtlt2 === ">") {
|
||||
gtlt2 = ">=";
|
||||
if (isXMinor) {
|
||||
major = +major + 1;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
} else {
|
||||
minor = +minor + 1;
|
||||
patch = 0;
|
||||
}
|
||||
} else if (gtlt2 === "<=") {
|
||||
gtlt2 = "<";
|
||||
if (isXMinor) {
|
||||
major = +major + 1;
|
||||
} else {
|
||||
minor = +minor + 1;
|
||||
}
|
||||
}
|
||||
if (gtlt2 === "<") {
|
||||
preRelease2 = "-0";
|
||||
}
|
||||
return `${gtlt2 + major}.${minor}.${patch}${preRelease2}`;
|
||||
} else if (isXMinor) {
|
||||
return `>=${major}.0.0${preRelease2} <${+major + 1}.0.0-0`;
|
||||
} else if (isXPatch) {
|
||||
return `>=${major}.${minor}.0${preRelease2} <${major}.${+minor + 1}.0-0`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
);
|
||||
}).join(" ");
|
||||
}
|
||||
function parseStar(range) {
|
||||
return range.trim().replace(parseRegex(star), "");
|
||||
}
|
||||
function parseGTE0(comparatorString) {
|
||||
return comparatorString.trim().replace(parseRegex(gte0), "");
|
||||
}
|
||||
function compareAtom(rangeAtom, versionAtom) {
|
||||
rangeAtom = +rangeAtom || rangeAtom;
|
||||
versionAtom = +versionAtom || versionAtom;
|
||||
if (rangeAtom > versionAtom) {
|
||||
return 1;
|
||||
}
|
||||
if (rangeAtom === versionAtom) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function comparePreRelease(rangeAtom, versionAtom) {
|
||||
const { preRelease: rangePreRelease } = rangeAtom;
|
||||
const { preRelease: versionPreRelease } = versionAtom;
|
||||
if (rangePreRelease === void 0 && !!versionPreRelease) {
|
||||
return 1;
|
||||
}
|
||||
if (!!rangePreRelease && versionPreRelease === void 0) {
|
||||
return -1;
|
||||
}
|
||||
if (rangePreRelease === void 0 && versionPreRelease === void 0) {
|
||||
return 0;
|
||||
}
|
||||
for (let i = 0, n = rangePreRelease.length; i <= n; i++) {
|
||||
const rangeElement = rangePreRelease[i];
|
||||
const versionElement = versionPreRelease[i];
|
||||
if (rangeElement === versionElement) {
|
||||
continue;
|
||||
}
|
||||
if (rangeElement === void 0 && versionElement === void 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!rangeElement) {
|
||||
return 1;
|
||||
}
|
||||
if (!versionElement) {
|
||||
return -1;
|
||||
}
|
||||
return compareAtom(rangeElement, versionElement);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function compareVersion(rangeAtom, versionAtom) {
|
||||
return compareAtom(rangeAtom.major, versionAtom.major) || compareAtom(rangeAtom.minor, versionAtom.minor) || compareAtom(rangeAtom.patch, versionAtom.patch) || comparePreRelease(rangeAtom, versionAtom);
|
||||
}
|
||||
function eq(rangeAtom, versionAtom) {
|
||||
return rangeAtom.version === versionAtom.version;
|
||||
}
|
||||
function compare(rangeAtom, versionAtom) {
|
||||
switch (rangeAtom.operator) {
|
||||
case "":
|
||||
case "=":
|
||||
return eq(rangeAtom, versionAtom);
|
||||
case ">":
|
||||
return compareVersion(rangeAtom, versionAtom) < 0;
|
||||
case ">=":
|
||||
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) < 0;
|
||||
case "<":
|
||||
return compareVersion(rangeAtom, versionAtom) > 0;
|
||||
case "<=":
|
||||
return eq(rangeAtom, versionAtom) || compareVersion(rangeAtom, versionAtom) > 0;
|
||||
case void 0: {
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function parseComparatorString(range) {
|
||||
return pipe(
|
||||
parseCarets,
|
||||
parseTildes,
|
||||
parseXRanges,
|
||||
parseStar
|
||||
)(range);
|
||||
}
|
||||
function parseRange(range) {
|
||||
return pipe(
|
||||
parseHyphen,
|
||||
parseComparatorTrim,
|
||||
parseTildeTrim,
|
||||
parseCaretTrim
|
||||
)(range.trim()).split(/\s+/).join(" ");
|
||||
}
|
||||
function satisfy(version, range) {
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
const parsedRange = parseRange(range);
|
||||
const parsedComparator = parsedRange.split(" ").map((rangeVersion) => parseComparatorString(rangeVersion)).join(" ");
|
||||
const comparators = parsedComparator.split(/\s+/).map((comparator2) => parseGTE0(comparator2));
|
||||
const extractedVersion = extractComparator(version);
|
||||
if (!extractedVersion) {
|
||||
return false;
|
||||
}
|
||||
const [
|
||||
,
|
||||
versionOperator,
|
||||
,
|
||||
versionMajor,
|
||||
versionMinor,
|
||||
versionPatch,
|
||||
versionPreRelease
|
||||
] = extractedVersion;
|
||||
const versionAtom = {
|
||||
version: combineVersion(
|
||||
versionMajor,
|
||||
versionMinor,
|
||||
versionPatch,
|
||||
versionPreRelease
|
||||
),
|
||||
major: versionMajor,
|
||||
minor: versionMinor,
|
||||
patch: versionPatch,
|
||||
preRelease: versionPreRelease == null ? void 0 : versionPreRelease.split(".")
|
||||
};
|
||||
for (const comparator2 of comparators) {
|
||||
const extractedComparator = extractComparator(comparator2);
|
||||
if (!extractedComparator) {
|
||||
return false;
|
||||
}
|
||||
const [
|
||||
,
|
||||
rangeOperator,
|
||||
,
|
||||
rangeMajor,
|
||||
rangeMinor,
|
||||
rangePatch,
|
||||
rangePreRelease
|
||||
] = extractedComparator;
|
||||
const rangeAtom = {
|
||||
operator: rangeOperator,
|
||||
version: combineVersion(
|
||||
rangeMajor,
|
||||
rangeMinor,
|
||||
rangePatch,
|
||||
rangePreRelease
|
||||
),
|
||||
major: rangeMajor,
|
||||
minor: rangeMinor,
|
||||
patch: rangePatch,
|
||||
preRelease: rangePreRelease == null ? void 0 : rangePreRelease.split(".")
|
||||
};
|
||||
if (!compare(rangeAtom, versionAtom)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const moduleMap = {};
|
||||
const moduleCache = Object.create(null);
|
||||
async function importShared(name, shareScope = 'default') {
|
||||
return moduleCache[name]
|
||||
? new Promise((r) => r(moduleCache[name]))
|
||||
: (await getSharedFromRuntime(name, shareScope)) || getSharedFromLocal(name)
|
||||
}
|
||||
async function getSharedFromRuntime(name, shareScope) {
|
||||
let module = null;
|
||||
if (globalThis?.__federation_shared__?.[shareScope]?.[name]) {
|
||||
const versionObj = globalThis.__federation_shared__[shareScope][name];
|
||||
const requiredVersion = moduleMap[name]?.requiredVersion;
|
||||
const hasRequiredVersion = !!requiredVersion;
|
||||
if (hasRequiredVersion) {
|
||||
const versionKey = Object.keys(versionObj).find((version) =>
|
||||
satisfy(version, requiredVersion)
|
||||
);
|
||||
if (versionKey) {
|
||||
const versionValue = versionObj[versionKey];
|
||||
module = await (await versionValue.get())();
|
||||
} else {
|
||||
console.log(
|
||||
`provider support ${name}(${versionKey}) is not satisfied requiredVersion(\${moduleMap[name].requiredVersion})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const versionKey = Object.keys(versionObj)[0];
|
||||
const versionValue = versionObj[versionKey];
|
||||
module = await (await versionValue.get())();
|
||||
}
|
||||
}
|
||||
if (module) {
|
||||
return flattenModule(module, name)
|
||||
}
|
||||
}
|
||||
async function getSharedFromLocal(name) {
|
||||
if (moduleMap[name]?.import) {
|
||||
let module = await (await moduleMap[name].get())();
|
||||
return flattenModule(module, name)
|
||||
} else {
|
||||
console.error(
|
||||
`consumer config import=false,so cant use callback shared module`
|
||||
);
|
||||
}
|
||||
}
|
||||
function flattenModule(module, name) {
|
||||
// use a shared module which export default a function will getting error 'TypeError: xxx is not a function'
|
||||
if (typeof module.default === 'function') {
|
||||
Object.keys(module).forEach((key) => {
|
||||
if (key !== 'default') {
|
||||
module.default[key] = module[key];
|
||||
}
|
||||
});
|
||||
moduleCache[name] = module.default;
|
||||
return module.default
|
||||
}
|
||||
if (module.default) module = Object.assign({}, module.default, module);
|
||||
moduleCache[name] = module;
|
||||
return module
|
||||
}
|
||||
|
||||
export { importShared, getSharedFromLocal as importSharedLocal, getSharedFromRuntime as importSharedRuntime };
|
||||
9
plugins.v2/clashruleprovider/dist/assets/_plugin-vue_export-helper-pcqpp-6-.js
vendored
Normal file
9
plugins.v2/clashruleprovider/dist/assets/_plugin-vue_export-helper-pcqpp-6-.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const _export_sfc = (sfc, props) => {
|
||||
const target = sfc.__vccOpts || sfc;
|
||||
for (const [key, val] of props) {
|
||||
target[key] = val;
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
export { _export_sfc as _ };
|
||||
87
plugins.v2/clashruleprovider/dist/assets/remoteEntry.js
vendored
Normal file
87
plugins.v2/clashruleprovider/dist/assets/remoteEntry.js
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
const currentImports = {};
|
||||
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
|
||||
let moduleMap = {
|
||||
"./Page":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Page-Bl7XNZ7k.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-DlQgf7u6.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Config":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Config-DXzIavcD.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-C3BpNVeC.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Dashboard":()=>{
|
||||
dynamicLoadingCss([], false, './Dashboard');
|
||||
return __federation_import('./__federation_expose_Dashboard-BkyO-3pr.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},};
|
||||
const seen = {};
|
||||
const dynamicLoadingCss = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
|
||||
const metaUrl = import.meta.url;
|
||||
if (typeof metaUrl === 'undefined') {
|
||||
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
|
||||
return;
|
||||
}
|
||||
|
||||
const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
|
||||
const base = '/';
|
||||
'assets';
|
||||
|
||||
cssFilePaths.forEach(cssPath => {
|
||||
let href = '';
|
||||
const baseUrl = base || curUrl;
|
||||
if (baseUrl) {
|
||||
const trimmer = {
|
||||
trailing: (path) => (path.endsWith('/') ? path.slice(0, -1) : path),
|
||||
leading: (path) => (path.startsWith('/') ? path.slice(1) : path)
|
||||
};
|
||||
const isAbsoluteUrl = (url) => url.startsWith('http') || url.startsWith('//');
|
||||
|
||||
const cleanBaseUrl = trimmer.trailing(baseUrl);
|
||||
const cleanCssPath = trimmer.leading(cssPath);
|
||||
const cleanCurUrl = trimmer.trailing(curUrl);
|
||||
|
||||
if (isAbsoluteUrl(baseUrl)) {
|
||||
href = [cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
|
||||
} else {
|
||||
if (cleanCurUrl.includes(cleanBaseUrl)) {
|
||||
href = [cleanCurUrl, cleanCssPath].filter(Boolean).join('/');
|
||||
} else {
|
||||
href = [cleanCurUrl + cleanBaseUrl, cleanCssPath].filter(Boolean).join('/');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
href = cssPath;
|
||||
}
|
||||
|
||||
if (dontAppendStylesToHead) {
|
||||
const key = 'css__ClashRuleProvider__' + exposeItemName;
|
||||
window[key] = window[key] || [];
|
||||
window[key].push(href);
|
||||
return;
|
||||
}
|
||||
|
||||
if (href in seen) return;
|
||||
seen[href] = true;
|
||||
|
||||
const element = document.createElement('link');
|
||||
element.rel = 'stylesheet';
|
||||
element.href = href;
|
||||
document.head.appendChild(element);
|
||||
});
|
||||
};
|
||||
async function __federation_import(name) {
|
||||
currentImports[name] ??= import(name);
|
||||
return currentImports[name]
|
||||
} const get =(module) => {
|
||||
if(!moduleMap[module]) throw new Error('Can not find remote module ' + module)
|
||||
return moduleMap[module]();
|
||||
};
|
||||
const init =(shareScope) => {
|
||||
globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
|
||||
Object.entries(shareScope).forEach(([key, value]) => {
|
||||
for (const [versionKey, versionValue] of Object.entries(value)) {
|
||||
const scope = versionValue.scope || 'default';
|
||||
globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
|
||||
const shared= globalThis.__federation_shared__[scope];
|
||||
(shared[key] = shared[key]||{})[versionKey] = versionValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { dynamicLoadingCss, get, init };
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import json
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
@@ -24,7 +25,7 @@ class ImdbSource(_PluginBase):
|
||||
plugin_icon = ("https://raw.githubusercontent.com/jxxghp/"
|
||||
"MoviePilot-Plugins/refs/heads/main/icons/IMDb_IOS-OSX_App.png")
|
||||
# 插件版本
|
||||
plugin_version = "1.3"
|
||||
plugin_version = "1.3.1"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -607,6 +608,8 @@ class ImdbSource(_PluginBase):
|
||||
elif year == "1970s":
|
||||
release_date_start = "1970-01-01"
|
||||
release_date_end = "1979-12-31"
|
||||
if not release_date_end:
|
||||
release_date_end = datetime.now().date().strftime("%Y-%m-%d")
|
||||
awards = (award,) if award else None
|
||||
ranked_lists = (ranked_list,) if ranked_list else None
|
||||
first_page = False
|
||||
@@ -644,17 +647,13 @@ class ImdbSource(_PluginBase):
|
||||
if mtype == "movies":
|
||||
for movie in results:
|
||||
movie_info = movie.get('node').get("title")
|
||||
pub_status = movie_info.get("productionStatus")
|
||||
if pub_status and pub_status.get("currentProductionStage"):
|
||||
if pub_status.get("currentProductionStage", {}).get("id") == 'released':
|
||||
res.append(self.__movie_to_media(movie_info))
|
||||
res.append(self.__movie_to_media(movie_info))
|
||||
|
||||
else:
|
||||
for tv in results:
|
||||
tv_info = tv.get('node').get('title')
|
||||
pub_status = tv_info.get("productionStatus")
|
||||
if pub_status and pub_status.get("currentProductionStage"):
|
||||
if pub_status.get("currentProductionStage", {}).get("id") == 'released':
|
||||
res.append(self.__series_to_media(tv_info))
|
||||
res.append(self.__series_to_media(tv_info))
|
||||
|
||||
return res
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -13,65 +13,72 @@
|
||||
- 支持批量翻译以提高效率
|
||||
- 支持使用滑动窗口配置上下文提高翻译连贯性
|
||||
- 支持多种字幕提取语言偏好设置
|
||||
- 支持监听媒体入库事件自动执行字幕生成
|
||||
- 支持手动触发字幕生成任务
|
||||
- 支持任务队列机制,确保并发安全
|
||||
- 支持任务状态列表展示(等待中 / 进行中 / 已完成 / 失败)
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基础配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 立即运行一次 | 保存配置后是否立即执行一次任务 | 否 |
|
||||
| 本地字幕提取策略 | 设置字幕提取的优先级策略 | 优先原音字幕 |
|
||||
| 翻译为中文 | 是否在需要时使用大模型将字幕翻译成中文 | 是 |
|
||||
| 发送通知 | 是否发送任务执行通知 | 否 |
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|----------|------------------------|--------|
|
||||
| 启用插件 | 是否启用插件 | 否 |
|
||||
| 清除历史记录 | 清除已完成的任务记录(完成、跳过或失败) | 否 |
|
||||
| 媒体入库自动执行 | 监听到媒体入库事件后自动执行字幕生成 | 是 |
|
||||
| 手动执行一次 | 保存配置后立即执行一次任务 | 否 |
|
||||
| 发送通知 | 是否发送任务执行通知 | 否 |
|
||||
| 文件大小(MB) | 最小处理的视频文件大小,小于该值的文件不处理 | 10 |
|
||||
| 字幕源语言偏好 | 设置字幕提取的优先级策略 | 优先原音字幕 |
|
||||
| 翻译为中文 | 是否使用大模型将字幕翻译成中文 | 是 |
|
||||
|
||||
### ASR配置
|
||||
### ASR配置(语音识别)
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 允许从音轨提取字幕 | 是否允许从视频音轨中提取字幕 | 是 |
|
||||
| ASR引擎 | 语音识别引擎 | faster-whisper |
|
||||
| 模型 | 使用的模型大小 | base |
|
||||
| 使用代理下载模型 | 是否使用代理下载模型 | 是 |
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|---------------------|------------------|------|
|
||||
| 允许从音轨提取字幕 | 是否允许从视频音轨中提取字幕 | 是 |
|
||||
| faster-whisper 模型选择 | 使用的 Whisper 模型大小 | base |
|
||||
| 使用代理下载模型 | 是否使用代理下载模型 | 是 |
|
||||
|
||||
### 翻译配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 启用批量翻译 | 是否启用批量翻译以提高效率 | 是 |
|
||||
| 每批翻译行数 | 每批处理的字幕行数 | 20 |
|
||||
| 上下文窗口大小 | 翻译时考虑的上下文行数 | 5 |
|
||||
| llm请求重试次数 | 翻译失败时的重试次数 | 3 |
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|-----------|----------------------|-----|
|
||||
| 启用批量翻译 | 是否启用批量翻译以提高效率 | 是 |
|
||||
| 每批翻译行数 | 每批处理的字幕行数 | 20 |
|
||||
| 上下文窗口大小 | 翻译时考虑的上下文行数 | 5 |
|
||||
| LLM请求重试次数 | 翻译失败时的重试次数 | 3 |
|
||||
| 翻译英文时合并整句 | 对英文字幕先合并单词再翻译,提升翻译质量 | 否 |
|
||||
|
||||
### 其他配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| 媒体路径 | 要处理的媒体文件或文件夹绝对路径,每行一个 | 空 |
|
||||
| 文件大小(MB) | 最小处理文件大小 | 10 |
|
||||
### 手动运行配置
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|------|-----------------------|-----|
|
||||
| 媒体路径 | 要处理的媒体文件或文件夹绝对路径,每行一个 | 空 |
|
||||
|
||||
## 字幕提取策略说明
|
||||
|
||||
字幕提取优先级:外挂字幕 > 内嵌字幕 > 音轨识别
|
||||
|
||||
字幕提取策略的选择主要取决于视频源语言和大模型的翻译能力。对于包含多语言字幕的非英语视频,建议根据以下原则选择策略:
|
||||
|
||||
1. 仅英文字幕
|
||||
- 仅使用英文字幕作为翻译源
|
||||
- 当视频无英文字幕时,使用ASR提取
|
||||
- 适用于大模型仅支持中英互译的场景
|
||||
- 仅使用英文字幕作为翻译源
|
||||
- 当视频无英文字幕时,使用ASR提取
|
||||
- 适用于大模型仅支持中英互译的场景
|
||||
|
||||
2. 优先英文字幕
|
||||
- 优先使用英文字幕作为翻译源
|
||||
- 无英文字幕时,使用其他语言字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型在英译中任务上表现更好的场景
|
||||
- 优先使用英文字幕作为翻译源
|
||||
- 无英文字幕时,使用其他语言字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型在英译中任务上表现更好的场景
|
||||
|
||||
3. 优先原音字幕
|
||||
- 优先使用视频原始语言的字幕
|
||||
- 无原音字幕时,使用英文字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型支持多语言翻译且翻译质量较好的场景
|
||||
- 优先使用视频原始语言的字幕
|
||||
- 无原音字幕时,使用英文字幕
|
||||
- 当所有字幕都不存在时,使用ASR提取
|
||||
- 适用于大模型支持多语言翻译且翻译质量较好的场景
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -79,10 +86,13 @@
|
||||
2. 首次使用音轨识别功能时,会自动从HuggingFace下载模型。开启"使用代理下载模型"选项会使用MP配置的代理。
|
||||
3. 媒体路径支持单个文件或文件夹的绝对路径。选择文件夹时会递归处理其中的所有视频文件,外挂字幕将从媒体文件同级目录中查找
|
||||
4. 批量翻译通过一次处理多行字幕来减少API调用次数,提高效率。如果翻译结果与原文行数不匹配,系统会自动降级为逐行翻译
|
||||
5. 上下文窗口大小和批量翻译行数需要根据大模型的推理能力来调整。当模型能力不足时,过大的批量或上下文窗口可能会影响翻译质量
|
||||
5. 上下文窗口大小和批量翻译行数需要根据大模型的推理能力来调整。当模型能力不足时,过大的批量或上下文窗口可能会影响翻译质量
|
||||
6. 翻译后的中文字幕会打上“机翻”标签。
|
||||
7. 插件运行时会启动一个后台线程用于消费任务队列,插件关闭时会清空队列并终止当前任务。
|
||||
|
||||
|
||||
## todo
|
||||
- 监听媒体入库事件自动调用字幕生成
|
||||
- 任务完成后调用媒体库刷新
|
||||
- 历史任务管理与展示
|
||||
|
||||
- 独立的大模型调用
|
||||
- 工作流/api接口
|
||||
- 任务完成后调用媒体库刷新
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import json
|
||||
import requests
|
||||
|
||||
@@ -15,10 +16,20 @@ class NtfyClient:
|
||||
headers = {
|
||||
"Title": title.encode(encoding='utf-8'),
|
||||
"Markdown": "true" if format_as_markdown else "false",
|
||||
"Icon": "https://movie-pilot.org/images/logo.png",
|
||||
}
|
||||
|
||||
if self._token:
|
||||
headers["Authorization"] = "Bearer " + self._token
|
||||
elif self._user and self._password:
|
||||
authStr = self._user + ":" + self._password
|
||||
headers["Authorization"] = "Basic " + base64.b64encode(authStr.encode('utf-8')).decode('utf-8')
|
||||
|
||||
if self._actions:
|
||||
headers["Actions"] = self._actions.encode('utf-8')
|
||||
|
||||
response = json.loads(
|
||||
requests.post(url=self.url, data=message.encode(encoding='utf-8'), headers=headers, auth=self._auth).text
|
||||
requests.post(url=self.url, data=message.encode(encoding='utf-8'), headers=headers).text
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -28,11 +39,16 @@ class NtfyClient:
|
||||
server: str = "https://ntfy.sh",
|
||||
user: str = "",
|
||||
password: str = "",
|
||||
token: str = "",
|
||||
actions: str = "",
|
||||
):
|
||||
self._server = server
|
||||
self._topic = topic
|
||||
self.__set_url(server, topic)
|
||||
self._auth = (user, password)
|
||||
self._user = user
|
||||
self._password = password
|
||||
self._token = token
|
||||
self._actions = actions
|
||||
|
||||
def __set_url(self, server, topic):
|
||||
self.url = server.strip("/") + "/" + topic
|
||||
@@ -46,7 +62,7 @@ class NtfyMsg(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Ntfy_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
plugin_version = "1.1"
|
||||
# 插件作者
|
||||
plugin_author = "lethargicScribe"
|
||||
# 作者主页
|
||||
@@ -64,6 +80,8 @@ class NtfyMsg(_PluginBase):
|
||||
_topic = None
|
||||
_user = None
|
||||
_password = None
|
||||
_token = None
|
||||
_actions = None
|
||||
_msgtypes = []
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
@@ -74,6 +92,8 @@ class NtfyMsg(_PluginBase):
|
||||
self._topic = config.get("topic")
|
||||
self._user = config.get("user")
|
||||
self._password = config.get("password")
|
||||
self._token = config.get("token")
|
||||
self._actions = config.get("actions")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled and (True if self._server and self._topic else False)
|
||||
@@ -194,6 +214,45 @@ class NtfyMsg(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'token',
|
||||
'label': '访问令牌',
|
||||
'placeholder': 'ntfytoken',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'actions',
|
||||
'label': '用户动作',
|
||||
'placeholder': 'ntfyactions',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
@@ -217,6 +276,48 @@ class NtfyMsg(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '用户或Token创建参考:https://docs.ntfy.sh/config/#access-control'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '用户动作创建参考:https://docs.ntfy.sh/publish/?h=action#using-a-header'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
], {
|
||||
@@ -226,6 +327,8 @@ class NtfyMsg(_PluginBase):
|
||||
'topic': 'MoviePilot',
|
||||
'user': '',
|
||||
'password': '',
|
||||
'token': '',
|
||||
'actions': '',
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
@@ -266,7 +369,11 @@ class NtfyMsg(_PluginBase):
|
||||
try:
|
||||
if not self._server or not self._topic:
|
||||
return False, "参数未配置"
|
||||
ntfy = NtfyClient(server=self._server, topic=self._topic, user=self._user, password=self._password)
|
||||
ntfy = NtfyClient(
|
||||
server=self._server, topic=self._topic,
|
||||
user=self._user, password=self._password,
|
||||
token=self._token, actions=self._actions
|
||||
)
|
||||
ntfy.send(title=title, message=text, format_as_markdown=True)
|
||||
|
||||
except Exception as msg_e:
|
||||
|
||||
Reference in New Issue
Block a user