Compare commits

...

5 Commits

Author SHA1 Message Date
jxxghp
8d30ba5c69 Merge pull request #996 from wumode/clashruleprovider 2026-03-08 07:46:10 +08:00
wumode
a9b66c4f43 fix(tobypasstrackers): improve torrents downloading 2026-03-07 22:41:12 +08:00
wumode
cdc062d681 fix(clashruleprovider): unable to delete proxies 2026-03-07 22:33:01 +08:00
jxxghp
437b2b05d4 Merge pull request #993 from honue/main 2026-02-25 15:13:11 +08:00
honue
944919fc34 feat: 共享识别词支持 JSON 格式远程识别词集合订阅 2026-02-25 14:47:56 +08:00
7 changed files with 146 additions and 34 deletions

View File

@@ -679,12 +679,13 @@
"name": "共享识别词",
"description": "从Github、Etherpad等远程文件中获取共享识别词并应用。",
"labels": "识别",
"version": "2.3",
"version": "2.4",
"icon": "words.png",
"author": "honue",
"level": 1,
"v2": true,
"history": {
"v2.4": "支持 JSON 格式远程识别词集合订阅",
"v2.3": "更换默认共享识别词地址"
}
},

View File

@@ -453,11 +453,12 @@
"name": "绕过Trackers",
"description": "提供tracker服务器IP地址列表帮助IPv6连接绕过OpenClash。",
"labels": "工具",
"version": "1.5.2",
"version": "1.5.3",
"icon": "Clash_A.png",
"author": "wumode",
"level": 2,
"history": {
"v1.5.3": "修复 Rousi 种子获取问题",
"v1.5.2": "支持从站点首页获取最新 Trackers",
"v1.5.1": "新增 Tracker",
"v1.5.0": "新增 Page 界面; 支持通过`/check_ip` 命令检查IP; 改进 UI",
@@ -514,12 +515,13 @@
"name": "Clash Rule Provider",
"description": "随时为Clash添加一些额外的规则。",
"labels": "工具",
"version": "2.1.2",
"version": "2.1.3",
"icon": "Mihomo_Meta_A.png",
"author": "wumode",
"level": 1,
"release": true,
"history": {
"v2.1.3": "修复代理删除问题",
"v2.1.2": "修复规则集序列化错误",
"v2.1.1": "增强数据管理功能",
"v2.0.10": "适配 MoviePilot 2.8.4",

View File

@@ -36,7 +36,7 @@ class ClashRuleProvider(_PluginBase):
# 插件图标
plugin_icon = "Mihomo_Meta_A.png"
# 插件版本
plugin_version = "2.1.2"
plugin_version = "2.1.3"
# 插件作者
plugin_author = "wumode"
# 作者主页

View File

@@ -10418,7 +10418,7 @@ const _sfc_main$d = /* @__PURE__ */ _defineComponent$d({
async function deleteProxy(name) {
loading.value = true;
try {
await props.api.delete(`/plugin/ClashRuleProvider/proxies/${name}`);
await props.api.delete(`/plugin/ClashRuleProvider/proxies/${encodeURIComponent(name)}`);
emit("refresh", ["proxies", "clash-outbounds"]);
} catch (err) {
if (err instanceof Error) {

View File

@@ -3,7 +3,7 @@ const currentImports = {};
let moduleMap = {
"./Page":()=>{
dynamicLoadingCss(["__federation_expose_Page-CJILOVp4.css"], false, './Page');
return __federation_import('./__federation_expose_Page-DhQfGEOD.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
return __federation_import('./__federation_expose_Page-DU-ODF61.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
"./Config":()=>{
dynamicLoadingCss(["__federation_expose_Config-CwbjkOP2.css"], false, './Config');
return __federation_import('./__federation_expose_Config-CY46uj5g.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},

View File

@@ -2,6 +2,7 @@ import asyncio
import base64
import ipaddress
import json
import re
import socket
import time
from datetime import datetime, timedelta
@@ -62,7 +63,7 @@ class ToBypassTrackers(_PluginBase):
# 插件图标
plugin_icon = "Clash_A.png"
# 插件版本
plugin_version = "1.5.2"
plugin_version = "1.5.3"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -715,7 +716,7 @@ class ToBypassTrackers(_PluginBase):
tracker_file = Path(self.get_data_path() / "trackers.json")
try:
if tracker_file.exists():
trackers: dict[str, list[str]] = json.loads(tracker_file.read_text())
trackers = json.loads(tracker_file.read_text(encoding="utf-8"))
else:
file = settings.ROOT_PATH / 'app' / 'plugins' / self.__class__.__name__.lower() / 'sites' / 'trackers'
with open(file, "r", encoding="utf-8") as f:
@@ -725,6 +726,63 @@ class ToBypassTrackers(_PluginBase):
logger.error(f"trackers 加载错误:{e}")
return trackers
@staticmethod
def _get_redict_url(url: str, ua: str | None = None, cookie: str | None = None) -> str | None:
"""
获取下载链接, url格式[base64]url
"""
# 获取[]中的内容
m = re.search(r"\[(.*)](.*)", url)
if m:
# 参数
base64_str = m.group(1)
# URL
url = m.group(2)
if not base64_str:
return url
# 解码参数
req_str = base64.b64decode(base64_str.encode('utf-8')).decode('utf-8')
req_params: Dict[str, dict] = json.loads(req_str)
# 是否使用cookie
if not req_params.get('cookie'):
cookie = None
# 代理
proxy = req_params.get('proxy')
# 请求头
if req_params.get('header'):
headers = req_params.get('header')
else:
headers = None
if req_params.get('method') == 'get':
# GET请求
res = RequestUtils(
ua=ua,
cookies=cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
).get_res(url, params=req_params.get('params'))
else:
# POST请求
res = RequestUtils(
ua=ua,
cookies=cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
).post_res(url, params=req_params.get('params'))
if not res:
return None
if not req_params.get('result'):
return res.text
else:
data = res.json()
for key in str(req_params.get('result')).split("."):
data = data.get(key)
if not data:
return None
logger.info(f"获取到下载地址:{data}")
return data
return None
def refresh_trackers(self):
"""更新 Tracker 服务器列表"""
logger.info("开始从站点获取最新 Tracker 服务器 ...")
@@ -735,7 +793,14 @@ class ToBypassTrackers(_PluginBase):
torrents = torrents_chain.browse(domain=site.domain)
if not torrents:
continue
torrent_url = torrents[0].enclosure
torrent_info = torrents[0]
torrent_url = torrent_info.enclosure
if torrent_url.startswith('['):
torrent_url = ToBypassTrackers._get_redict_url(
url=torrent_url, ua=torrent_info.site_ua, cookie=torrent_info.site_cookie
)
if torrent_url is None:
continue
_, content, _, _, error_msg = TorrentHelper().download_torrent(
url=torrent_url,
cookie=site.cookie,
@@ -748,16 +813,17 @@ class ToBypassTrackers(_PluginBase):
except BencodeDecodingError as e:
logger.error(f"解析 {site.name} 种子文件失败: {e}")
continue
servers: list[str] = []
for urls in torrent.announce_urls:
servers: set[str] = set()
for urls in torrent.announce_urls or []:
for url in urls:
parsed = urlparse(url)
if parsed.hostname:
servers.append(parsed.hostname)
servers.add(parsed.hostname)
if servers:
trackers[site.domain] = servers
trackers[site.domain] = list(servers)
logger.info(f"更新 {site.name} trackers -> {trackers[site.domain]}")
tracker_file = Path(self.get_data_path() / "trackers.json")
tracker_file.write_text(json.dumps(trackers, indent=4))
tracker_file.write_text(json.dumps(trackers, indent=4), encoding="utf-8")
logger.info("已更新 Tracker 服务器列表")
def bypassed_ips(self, protocol: Literal['4', '6']) -> Response:
@@ -941,7 +1007,7 @@ class ToBypassTrackers(_PluginBase):
for domain in site_domains:
domain_name_map[domain] = do_sites[site]
else:
logger.warn(f"不支持的站点: {do_sites[site]}({site})")
logger.warn(f"不支持的站点: {do_sites[site]}({site})请执行一次trackers更新。")
unsupported_msg.append(f'{do_sites[site]}】不支持的站点')
for custom_tracker in self._custom_trackers.split('\n'):
if custom_tracker:

View File

@@ -1,3 +1,4 @@
import json
from typing import List, Tuple, Dict, Any
import datetime
@@ -23,7 +24,7 @@ class RemoteIdentifiers(_PluginBase):
# 插件图标
plugin_icon = "words.png"
# 插件版本
plugin_version = "2.3"
plugin_version = "2.4"
# 插件作者
plugin_author = "honue"
# 作者主页
@@ -74,25 +75,17 @@ class RemoteIdentifiers(_PluginBase):
def get_file_content(self, file_urls: list) -> List[str]:
ret: List[str] = ['#========以下识别词由 RemoteIdentifiers 插件添加========#']
for file_url in file_urls:
# https://movie-pilot.org/etherpad/p/MoviePilot_TV_Words
if file_url.count("etherpad") != 0 and file_url.count("export") == 0:
real_url = file_url + "/export/txt"
file_url = file_url.strip()
if not file_url:
continue
if file_url.lower().endswith(".json"):
mapping = self.__get_remote_mapping(file_url=file_url)
for words_name, words_url in mapping.items():
identifiers = self.__get_remote_identifiers(words_url=words_url, words_name=words_name)
ret += identifiers
else:
real_url = file_url
response = RequestUtils(proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS if real_url.count("github") else None,
timeout=15).get_res(real_url)
if not response:
raise Exception(f"文件 {real_url} 下载失败!")
elif response.status_code != 200:
raise Exception(f"下载文件 {real_url} 失败:{response.status_code} - {response.reason}")
text = response.content.decode('utf-8')
if text.find("doctype html") > 0:
raise Exception(f"下载文件 {real_url} 失败:{response.status_code} - {response.reason}")
if "try again later" in text:
raise Exception(f"下载文件 {real_url} 失败:{text}")
identifiers: List[str] = text.split('\n')
ret += identifiers
identifiers = self.__get_remote_identifiers(words_url=file_url)
ret += identifiers
# flitter 过滤空行
if self._flitter:
filtered_ret = []
@@ -103,6 +96,56 @@ class RemoteIdentifiers(_PluginBase):
logger.info(f"获取到远端识别词{len(ret) - 1}条: {ret[1:]}")
return ret
def __get_real_url(self, words_url: str) -> str:
# https://movie-pilot.org/etherpad/p/MoviePilot_TV_Words
if words_url.count("etherpad") != 0 and words_url.count("export") == 0:
return words_url + "/export/txt"
return words_url
def __get_response_text(self, url: str) -> str:
response = RequestUtils(
proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS if url.count("github") else None,
timeout=15
).get_res(url)
if not response:
raise Exception(f"文件 {url} 下载失败!")
if response.status_code != 200:
raise Exception(f"下载文件 {url} 失败:{response.status_code} - {response.reason}")
text = response.content.decode('utf-8')
if text.find("doctype html") > 0:
raise Exception(f"下载文件 {url} 失败:{response.status_code} - {response.reason}")
if "try again later" in text:
raise Exception(f"下载文件 {url} 失败:{text}")
return text
def __get_remote_identifiers(self, words_url: str, words_name: str = None) -> List[str]:
real_url = self.__get_real_url(words_url=words_url)
text = self.__get_response_text(url=real_url)
identifiers = text.split('\n')
if words_name:
logger.info(f"词表[{words_name}]获取成功,地址:{real_url},识别词数量:{len(identifiers)}")
return identifiers
def __get_remote_mapping(self, file_url: str) -> Dict[str, str]:
real_url = self.__get_real_url(words_url=file_url)
text = self.__get_response_text(url=real_url)
try:
mapping = json.loads(text)
except json.JSONDecodeError as e:
raise Exception(f"订阅文件 {real_url} 不是合法JSON{str(e)}")
if not isinstance(mapping, dict):
raise Exception(f"订阅文件 {real_url} 格式错误:必须为对象,格式为 词表名 -> 词表地址")
normalized_mapping: Dict[str, str] = {}
for words_name, words_url in mapping.items():
if not isinstance(words_name, str):
raise Exception(f"订阅文件 {real_url} 格式错误:词表名必须是字符串")
if not isinstance(words_url, str) or not words_url.strip():
raise Exception(f"订阅文件 {real_url} 格式错误:词表[{words_name}]地址必须是非空字符串")
normalized_mapping[words_name] = words_url.strip()
logger.info(f"订阅文件[{real_url}]解析成功,共 {len(normalized_mapping)} 个词表")
return normalized_mapping
def __task(self):
words: List[str] = self.systemconfig.get(SystemConfigKey.CustomIdentifiers) or []
file_urls: list = self._file_urls.split('\n') if self._file_urls else []