Merge pull request #26 from thsrite/main

This commit is contained in:
jxxghp 2023-08-04 12:24:59 +08:00 committed by GitHub
commit fca69d1dbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 689 additions and 5 deletions

View File

@ -38,8 +38,8 @@ jobs:
cd MoviePilot-Frontend cd MoviePilot-Frontend
yarn && yarn build yarn && yarn build
cd .. cd ..
mkdir -p public mkdir -p /public
cp -rf ./MoviePilot-Frontend/dist/* ./public/ cp -rf ./MoviePilot-Frontend/dist/* /public/
rm -rf MoviePilot-Frontend rm -rf MoviePilot-Frontend
chmod +x start.sh chmod +x start.sh

View File

@ -0,0 +1,684 @@
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Tuple, Dict, Any
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from python_hosts import Hosts, HostsEntry
from app.core.config import settings
from app.log import logger
from app.plugins import _PluginBase
from app.schemas.types import EventType
from app.utils.http import RequestUtils
from app.utils.ip import IpUtils
from app.utils.system import SystemUtils
class CloudflareSpeedTest(_PluginBase):
# 插件名称
plugin_name = "Cloudflare IP优选"
# 插件描述
plugin_desc = "🌩 测试 Cloudflare CDN 延迟和速度自动优选IP。"
# 插件图标
plugin_icon = "cloudflare.png"
# 主题色
plugin_color = "#F6821F"
# 插件版本
plugin_version = "1.0"
# 插件作者
plugin_author = "thsrite"
# 作者主页
author_url = "https://github.com/thsrite"
# 插件配置项ID前缀
plugin_config_prefix = "cloudflarespeedtest_"
# 加载顺序
plugin_order = 12
# 可使用的用户级别
auth_level = 1
# 私有属性
_customhosts = False
_cf_ip = None
_scheduler = None
_cron = None
_onlyonce = False
_ipv4 = False
_ipv6 = False
_version = None
_additional_args = None
_re_install = False
_notify = False
_check = False
_cf_path = None
_cf_ipv4 = None
_cf_ipv6 = None
_result_file = None
_release_prefix = 'https://github.com/XIU2/CloudflareSpeedTest/releases/download'
_binary_name = 'CloudflareST'
def init_plugin(self, config: dict = None):
# 停止现有任务
self.stop_service()
# 读取配置
if config:
self._onlyonce = config.get("onlyonce")
self._cron = config.get("cron")
self._cf_ip = config.get("cf_ip")
self._version = config.get("version")
self._ipv4 = config.get("ipv4")
self._ipv6 = config.get("ipv6")
self._re_install = config.get("re_install")
self._additional_args = config.get("additional_args")
self._notify = config.get("notify")
self._check = config.get("check")
if self.get_state() or self._onlyonce:
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
if self._cron:
logger.info(f"Cloudflare CDN优选服务启动周期{self._cron}")
self._scheduler.add_job(func=self.__cloudflareSpeedTest,
trigger=CronTrigger.from_crontab(self._cron),
name="Cloudflare优选")
if self._onlyonce:
logger.info(f"Cloudflare CDN优选服务启动立即运行一次")
self._scheduler.add_job(self.__cloudflareSpeedTest, 'date',
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3))
# 关闭一次性开关
self._onlyonce = False
self.__update_config()
# 启动任务
if self._scheduler.get_jobs():
self._scheduler.print_jobs()
self._scheduler.start()
def __cloudflareSpeedTest(self):
"""
CloudflareSpeedTest优选
"""
self._cf_path = self.get_data_path()
self._cf_ipv4 = os.path.join(self._cf_path, "ip.txt")
self._cf_ipv6 = os.path.join(self._cf_path, "ipv6.txt")
self._result_file = os.path.join(self._cf_path, "result_hosts.txt")
# 获取自定义Hosts插件若无设置则停止
customHosts = self.get_config("CustomHosts")
self._customhosts = customHosts and customHosts.get("enable")
if self._cf_ip and not customHosts or not customHosts.get("hosts"):
logger.error(f"Cloudflare CDN优选依赖于自定义Hosts请先维护hosts")
return
if not self._cf_ip:
logger.error("CloudflareSpeedTest加载成功首次运行需要配置优选ip")
return
# ipv4和ipv6必须其一
if not self._ipv4 and not self._ipv6:
self._ipv4 = True
self.__update_config()
logger.warn(f"Cloudflare CDN优选未指定ip类型默认ipv4")
err_flag, release_version = self.__check_envirment()
if err_flag and release_version:
# 更新版本
self._version = release_version
self.__update_config()
hosts = customHosts.get("hosts")
if isinstance(hosts, str):
hosts = str(hosts).split('\n')
# 校正优选ip
if self._check:
self.__check_cf_if(hosts=hosts)
# 开始优选
if err_flag:
logger.info("正在进行CLoudflare CDN优选请耐心等待")
# 执行优选命令,-dd不测速
cf_command = f'cd {self._cf_path} && chmod a+x {self._binary_name} && ./{self._binary_name} {self._additional_args} -o {self._result_file}' + (
f' -f {self._cf_ipv4}' if self._ipv4 else '') + (f' -f {self._cf_ipv6}' if self._ipv6 else '')
logger.info(f'正在执行优选命令 {cf_command}')
os.system(cf_command)
# 获取优选后最优ip
best_ip = SystemUtils.execute("sed -n '2,1p' " + self._result_file + " | awk -F, '{print $1}'")
logger.info(f"\n获取到最优ip==>[{best_ip}]")
# 替换自定义Hosts插件数据库hosts
if IpUtils.is_ipv4(best_ip) or IpUtils.is_ipv6(best_ip):
if best_ip == self._cf_ip:
logger.info(f"CloudflareSpeedTest CDN优选ip未变不做处理")
else:
# 替换优选ip
err_hosts = customHosts.get("err_hosts")
enable = customHosts.get("enable")
# 处理ip
new_hosts = []
for host in hosts:
if host and host != '\n':
host_arr = str(host).split()
if host_arr[0] == self._cf_ip:
new_hosts.append(host.replace(self._cf_ip, best_ip))
else:
new_hosts.append(host)
# 更新自定义Hosts
self.update_config(
{
"hosts": new_hosts,
"err_hosts": err_hosts,
"enable": enable
}, "CustomHosts"
)
# 更新优选ip
old_ip = self._cf_ip
self._cf_ip = best_ip
self.__update_config()
logger.info(f"Cloudflare CDN优选ip [{best_ip}] 已替换自定义Hosts插件")
# 解发自定义hosts插件重载
logger.info("通知CustomHosts插件重载 ...")
self.eventmanager.send_event(EventType.PluginReload,
{
"plugin_id": "CustomHosts"
})
if self._notify:
logger.send_message(
title="【Cloudflare优选任务完成】",
text=f"原ip{old_ip}\n"
f"新ip{best_ip}"
)
else:
logger.error("获取到最优ip格式错误请重试")
self._onlyonce = False
self.__update_config()
self.stop_service()
def __check_cf_if(self, hosts):
"""
校正cf优选ip
防止特殊情况下cf优选ip和自定义hosts插件中ip不一致
"""
# 统计每个IP地址出现的次数
ip_count = {}
for host in hosts:
ip = host.split()[0]
if ip in ip_count:
ip_count[ip] += 1
else:
ip_count[ip] = 1
# 找出出现次数最多的IP地址
max_ips = [] # 保存最多出现的IP地址
max_count = 0
for ip, count in ip_count.items():
if count > max_count:
max_ips = [ip] # 更新最多的IP地址
max_count = count
elif count == max_count:
max_ips.append(ip)
# 如果出现次数最多的ip不止一个则不做兼容处理
if len(max_ips) != 1:
return
if max_ips[0] != self._cf_ip:
self._cf_ip = max_ips[0]
logger.info(f"获取到自定义hosts插件中ip {max_ips[0]} 出现次数最多已自动校正优选ip")
def __check_envirment(self):
"""
环境检查
"""
# 是否安装标识
install_flag = False
# 是否重新安装
if self._re_install:
install_flag = True
os.system(f'rm -rf {self._cf_path}')
logger.info(f'删除CloudflareSpeedTest目录 {self._cf_path},开始重新安装')
# 判断目录是否存在
cf_path = Path(self._cf_path)
if not cf_path.exists():
os.mkdir(self._cf_path)
# 获取CloudflareSpeedTest最新版本
release_version = self.__get_release_version()
if not release_version:
# 如果升级失败但是有可执行文件CloudflareST则可继续运行反之停止
if Path(f'{self._cf_path}/{self._binary_name}').exists():
logger.warn(f"获取CloudflareSpeedTest版本失败存在可执行版本继续运行")
return True, None
elif self._version:
logger.error(f"获取CloudflareSpeedTest版本失败获取上次运行版本{self._version},开始安装")
install_flag = True
else:
release_version = "v2.2.2"
self._version = release_version
logger.error(f"获取CloudflareSpeedTest版本失败获取默认版本{release_version},开始安装")
install_flag = True
# 有更新
if not install_flag and release_version != self._version:
logger.info(f"检测到CloudflareSpeedTest有版本[{release_version}]更新,开始安装")
install_flag = True
# 重装后数据库有版本数据,但是本地没有则重装
if not install_flag and release_version == self._version and not Path(
f'{self._cf_path}/{self._binary_name}').exists():
logger.warn(f"未检测到CloudflareSpeedTest本地版本重新安装")
install_flag = True
if not install_flag:
logger.info(f"CloudflareSpeedTest无新版本存在可执行版本继续运行")
return True, None
# 检查环境、安装
if SystemUtils.is_windows():
# todo
logger.error(f"CloudflareSpeedTest暂不支持windows平台")
return False, None
elif SystemUtils.is_macos():
# mac
uname = SystemUtils.execute('uname -m')
arch = 'amd64' if uname == 'x86_64' else 'arm64'
cf_file_name = f'CloudflareST_darwin_{arch}.zip'
download_url = f'{self._release_prefix}/{release_version}/{cf_file_name}'
return self.__os_install(download_url, cf_file_name, release_version,
f"ditto -V -x -k --sequesterRsrc {self._cf_path}/{cf_file_name} {self._cf_path}")
else:
# docker
uname = SystemUtils.execute('uname -m')
arch = 'amd64' if uname == 'x86_64' else 'arm64'
cf_file_name = f'CloudflareST_linux_{arch}.tar.gz'
download_url = f'{self._release_prefix}/{release_version}/{cf_file_name}'
return self.__os_install(download_url, cf_file_name, release_version,
f"tar -zxf {self._cf_path}/{cf_file_name} -C {self._cf_path}")
def __os_install(self, download_url, cf_file_name, release_version, unzip_command):
"""
macos docker安装cloudflare
"""
# 手动下载安装包后,无需在此下载
if not Path(f'{self._cf_path}/{cf_file_name}').exists():
# 首次下载或下载新版压缩包
proxies = settings.PROXY
https_proxy = proxies.get("https") if proxies and proxies.get("https") else None
if https_proxy:
os.system(
f'wget -P {self._cf_path} --no-check-certificate -e use_proxy=yes -e https_proxy={https_proxy} {download_url}')
else:
os.system(f'wget -P {self._cf_path} https://ghproxy.com/{download_url}')
# 判断是否下载好安装包
if Path(f'{self._cf_path}/{cf_file_name}').exists():
try:
# 解压
os.system(f'{unzip_command}')
# 删除压缩包
os.system(f'rm -rf {self._cf_path}/{cf_file_name}')
if Path(f'{self._cf_path}/{self._binary_name}').exists():
logger.info(f"CloudflareSpeedTest安装成功当前版本{release_version}")
return True, release_version
else:
logger.error(f"CloudflareSpeedTest安装失败请检查")
os.removedirs(self._cf_path)
return False, None
except Exception as err:
# 如果升级失败但是有可执行文件CloudflareST则可继续运行反之停止
if Path(f'{self._cf_path}/{self._binary_name}').exists():
logger.error(f"CloudflareSpeedTest安装失败{str(err)},继续使用现版本运行")
return True, None
else:
logger.error(f"CloudflareSpeedTest安装失败{str(err)},无可用版本,停止运行")
os.removedirs(self._cf_path)
return False, None
else:
# 如果升级失败但是有可执行文件CloudflareST则可继续运行反之停止
if Path(f'{self._cf_path}/{self._binary_name}').exists():
logger.warn(f"CloudflareSpeedTest安装失败存在可执行版本继续运行")
return True, None
else:
logger.error(f"CloudflareSpeedTest安装失败无可用版本停止运行")
os.removedirs(self._cf_path)
return False, None
@staticmethod
def __get_release_version():
"""
获取CloudflareSpeedTest最新版本
"""
version_res = RequestUtils().get_res(
"https://api.github.com/repos/XIU2/CloudflareSpeedTest/releases/latest")
if not version_res:
version_res = RequestUtils(proxies=settings.PROXY).get_res(
"https://api.github.com/repos/XIU2/CloudflareSpeedTest/releases/latest")
if version_res:
ver_json = version_res.json()
version = f"{ver_json['tag_name']}"
return version
else:
return None
def __update_config(self):
"""
更新优选插件配置
"""
self.update_config({
"onlyonce": False,
"cron": self._cron,
"cf_ip": self._cf_ip,
"version": self._version,
"ipv4": self._ipv4,
"ipv6": self._ipv6,
"re_install": self._re_install,
"additional_args": self._additional_args,
"notify": self._notify,
"check": self._check
})
def get_state(self) -> bool:
return self._cf_ip and True if self._cron else False
@staticmethod
def get_command() -> List[Dict[str, Any]]:
pass
def get_api(self) -> List[Dict[str, Any]]:
pass
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1页面配置2数据结构
"""
return [
{
'component': 'VForm',
'content': [
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cf_ip',
'label': '优选IP',
'placeholder': '121.121.121.121'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'cron',
'label': '优选周期',
'placeholder': '0 0 0 ? *'
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'version',
'readonly': True,
'label': 'CloudflareSpeedTest版本',
'placeholder': '暂未安装'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'ipv4',
'label': 'IPv4',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'ipv6',
'label': 'IPv6',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'check',
'label': '自动校准',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'onlyonce',
'label': '立即运行一次',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 're_install',
'label': '重装后运行',
}
}
]
},
{
'component': 'VCol',
'props': {
'cols': 12,
'md': 4
},
'content': [
{
'component': 'VSwitch',
'props': {
'model': 'notify',
'label': '运行时通知',
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12
},
'content': [
{
'component': 'VTextField',
'props': {
'model': 'additional_args',
'label': '高级参数',
'placeholder': '-dd'
}
}
]
}
]
}
]
}
], {
"cf_ip": "",
"cron": "",
"version": "",
"ipv4": True,
"ipv6": False,
"check": False,
"onlyonce": False,
"re_install": False,
"notify": True,
"additional_args": ""
}
def get_page(self) -> List[dict]:
pass
@staticmethod
def __read_system_hosts():
"""
读取系统hosts对象
"""
# 获取本机hosts路径
if SystemUtils.is_windows():
hosts_path = r"c:\windows\system32\drivers\etc\hosts"
else:
hosts_path = '/etc/hosts'
# 读取系统hosts
return Hosts(path=hosts_path)
def __add_hosts_to_system(self, hosts):
"""
添加hosts到系统
"""
# 系统hosts对象
system_hosts = self.__read_system_hosts()
# 过滤掉插件添加的hosts
orgin_entries = []
for entry in system_hosts.entries:
if entry.entry_type == "comment" and entry.comment == "# CustomHostsPlugin":
break
orgin_entries.append(entry)
system_hosts.entries = orgin_entries
# 新的有效hosts
new_entrys = []
# 新的错误的hosts
err_hosts = []
err_flag = False
for host in hosts:
if not host:
continue
host_arr = str(host).split()
try:
host_entry = HostsEntry(entry_type='ipv4' if IpUtils.is_ipv4(str(host_arr[0])) else 'ipv6',
address=host_arr[0],
names=host_arr[1:])
new_entrys.append(host_entry)
except Exception as err:
err_hosts.append(host + "\n")
logger.error(f"{host} 格式转换错误:{str(err)}")
# 推送实时消息
self.systemmessage.put(f"{host} 格式转换错误:{str(err)}")
# 写入系统hosts
if new_entrys:
try:
# 添加分隔标识
system_hosts.add([HostsEntry(entry_type='comment', comment="# CustomHostsPlugin")])
# 添加新的Hosts
system_hosts.add(new_entrys)
system_hosts.write()
logger.info("更新系统hosts文件成功")
except Exception as err:
err_flag = True
logger.error(f"更新系统hosts文件失败{str(err) or '请检查权限'}")
# 推送实时消息
self.systemmessage.put(f"更新系统hosts文件失败{str(err) or '请检查权限'}")
return err_flag, err_hosts
def stop_service(self):
"""
退出插件
"""
try:
if self._scheduler:
self._scheduler.remove_all_jobs()
if self._scheduler.running:
self._scheduler.shutdown()
self._scheduler = None
except Exception as e:
logger.error("退出插件失败:%s" % str(e))

View File

@ -56,7 +56,7 @@ class DirMonitor(_PluginBase):
# 加载顺序 # 加载顺序
plugin_order = 4 plugin_order = 4
# 可使用的用户级别 # 可使用的用户级别
user_level = 1 auth_level = 1
# 已处理的文件清单 # 已处理的文件清单
_synced_files = [] _synced_files = []

View File

@ -30,7 +30,7 @@ http {
# 主目录 # 主目录
expires off; expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Cache-Control "no-cache, no-store, must-revalidate";
root /app/public; root /public;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
@ -38,7 +38,7 @@ http {
# 静态资源 # 静态资源
expires 7d; expires 7d;
add_header Cache-Control "public"; add_header Cache-Control "public";
root /app/public; root /public;
} }
location /api/v1/site/icon/ { location /api/v1/site/icon/ {