feat 在线仓库插件安装

This commit is contained in:
jxxghp
2023-11-01 20:56:38 +08:00
parent 0dac3f1b1d
commit fbe306ba90
7 changed files with 238 additions and 21 deletions

View File

@ -6,6 +6,7 @@ from app import schemas
from app.core.plugin import PluginManager
from app.core.security import verify_token
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.plugin import PluginHelper
from app.schemas.types import SystemConfigKey
router = APIRouter()
@ -16,7 +17,25 @@ def all_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询所有插件清单
"""
return PluginManager().get_plugin_apps()
# 查询本地插件
local_plugins = PluginManager().get_local_plugins()
# 在线插件
online_plugins = PluginManager().get_online_plugins()
# 全并去重,在线插件有的以在线插件为准
plugins = []
if not local_plugins:
return online_plugins
for plugin in local_plugins:
for online_plugin in online_plugins:
if plugin["id"] == online_plugin["id"]:
plugins.append(online_plugin)
break
else:
plugins.append(plugin)
for plugin in online_plugins:
if plugin not in plugins:
plugins.append(plugin)
return plugins
@router.get("/installed", summary="已安装插件", response_model=List[str])
@ -29,18 +48,34 @@ def installed_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/install/{plugin_id}", summary="安装插件", response_model=schemas.Response)
def install_plugin(plugin_id: str,
repo_url: str = "",
force: bool = False,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
安装插件
"""
# 已安装插件
install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
# 重载标志
reload_flag = False
# 如果是非本地括件,或者强制安装时,则需要下载安装
if repo_url and (force or plugin_id not in PluginManager().get_plugin_ids()):
# 下载安装
state, msg = PluginHelper().install(pid=plugin_id, repo_url=repo_url)
if state:
# 安装成功
reload_flag = True
else:
# 安装失败
return schemas.Response(success=False, msg=msg)
# 安装插件
if plugin_id not in install_plugins:
reload_flag = True
install_plugins.append(plugin_id)
# 保存设置
SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins)
# 重载插件管理器
# 重载插件管理器
if reload_flag:
PluginManager().init_config()
return schemas.Response(success=True)

View File

@ -210,10 +210,8 @@ class Settings(BaseSettings):
OVERWRITE_MODE: str = "size"
# 大内存模式
BIG_MEMORY_MODE: bool = False
# 插件市场地址
PLUGIN_MARKET: str = "https://movie-pilot.org/pluginmarket"
# 资源包更新地址
RESOURCE_HOST: str = "https://movie-pilot.org/resources"
# 插件市场仓库地址,多个地址使用,分隔,地址以/结尾
PLUGIN_MARKET: str = "https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/"
@property
def INNER_CONFIG_PATH(self):

View File

@ -1,14 +1,17 @@
import traceback
from typing import List, Any, Dict, Tuple
from app.core.config import settings
from app.core.event import eventmanager
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.module import ModuleHelper
from app.helper.plugin import PluginHelper
from app.helper.sites import SitesHelper
from app.log import logger
from app.schemas.types import SystemConfigKey
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
class PluginManager(metaclass=Singleton):
@ -26,6 +29,7 @@ class PluginManager(metaclass=Singleton):
def __init__(self):
self.siteshelper = SitesHelper()
self.pluginhelper = PluginHelper()
self.init_config()
def init_config(self):
@ -188,9 +192,91 @@ class PluginManager(metaclass=Singleton):
"""
return list(self._plugins.keys())
def get_plugin_apps(self) -> List[dict]:
def get_online_plugins(self) -> List[Dict[str, dict]]:
"""
获取所有插件信息
获取所有在线插件信息
"""
# 返回值
all_confs = []
if not settings.PLUGIN_MARKET:
return all_confs
# 已安装插件
installed_apps = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or []
# 线上插件列表
markets = settings.PLUGIN_MARKET.split(",")
for market in markets:
online_plugins = self.pluginhelper.get_plugins(market) or {}
for pid, plugin in online_plugins.items():
# 运行状插件
plugin_obj = self._running_plugins.get(pid)
# 非运行态插件
plugin_static = self._plugins.get(pid)
# 基本属性
conf = {}
# ID
conf.update({"id": pid})
# 安装状态,是否有新版本
if plugin_static:
# 已安装
if pid in installed_apps:
conf.update({"installed": True})
else:
conf.update({"installed": False})
conf.update({"has_update": False})
if plugin_obj:
installed_version = getattr(plugin_static, "plugin_version")
if StringUtils.compare_version(installed_version, plugin.get("version")) < 0:
# 需要更新
conf.update({"installed": False})
conf.update({"has_update": True})
else:
# 未安装
conf.update({"installed": False})
conf.update({"has_update": False})
# 运行状态
if plugin_obj and hasattr(plugin_obj, "get_state"):
conf.update({"state": plugin_obj.get_state()})
else:
conf.update({"state": False})
# 是否有详情页面
conf.update({"has_page": False})
if plugin_obj and hasattr(plugin_obj, "get_page"):
if ObjectUtils.check_method(plugin_obj.get_page):
conf.update({"has_page": True})
# 权限
if plugin.get("level"):
conf.update({"auth_level": plugin.get("level")})
if self.siteshelper.auth_level < plugin.get("level"):
continue
# 名称
if plugin.get("name"):
conf.update({"plugin_name": plugin.get("name")})
# 描述
if plugin.get("description"):
conf.update({"plugin_desc": plugin.get("description")})
# 版本
if plugin.get("version"):
conf.update({"plugin_version": plugin.get("version")})
# 图标
if plugin.get("icon"):
conf.update({"plugin_icon": plugin.get("icon")})
# 主题色
if plugin.get("color"):
conf.update({"plugin_color": plugin.get("color")})
# 作者
if plugin.get("author"):
conf.update({"plugin_author": plugin.get("author")})
# 仓库链接
conf.update({"repo_url": market})
# 本地标志
conf.update({"is_local": False})
# 汇总
all_confs.append(conf)
return all_confs
def get_local_plugins(self) -> List[Dict[str, dict]]:
"""
获取所有本地已下载的插件信息
"""
# 返回值
all_confs = []
@ -209,7 +295,7 @@ class PluginManager(metaclass=Singleton):
else:
conf.update({"installed": False})
# 运行状态
if plugin_obj and hasattr(plugin, "get_state"):
if plugin_obj and hasattr(plugin_obj, "get_state"):
conf.update({"state": plugin_obj.get_state()})
else:
conf.update({"state": False})
@ -221,6 +307,7 @@ class PluginManager(metaclass=Singleton):
conf.update({"has_page": False})
# 权限
if hasattr(plugin, "auth_level"):
conf.update({"auth_level": plugin.auth_level})
if self.siteshelper.auth_level < plugin.auth_level:
continue
# 名称
@ -244,6 +331,10 @@ class PluginManager(metaclass=Singleton):
# 作者链接
if hasattr(plugin, "author_url"):
conf.update({"author_url": plugin.author_url})
# 是否需要更新
conf.update({"has_update": False})
# 本地标志
conf.update({"is_local": True})
# 汇总
all_confs.append(conf)
return all_confs

View File

@ -1,9 +1,14 @@
import json
import shutil
from pathlib import Path
from typing import Dict
from typing import Dict, Tuple
from cachetools import TTLCache, cached
from app.core.config import settings
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.system import SystemUtils
class PluginHelper(metaclass=Singleton):
@ -12,20 +17,77 @@ class PluginHelper(metaclass=Singleton):
"""
@cached(cache=TTLCache(maxsize=1, ttl=1800))
def get_plugins(self) -> Dict[str, dict]:
def get_plugins(self, repo_url: str) -> Dict[str, dict]:
"""
获取Github所有最新插件列表
:param repo_url: Github仓库地址
"""
pass
if not repo_url:
return {}
res = RequestUtils(proxies=settings.PROXY).get_res(f"{repo_url}package.json")
if res:
return json.loads(res.text)
return {}
def download(self, name: str, dest: Path) -> bool:
"""
下载插件到本地
"""
pass
def install(self, name: str) -> bool:
@staticmethod
def install(pid: str, repo_url: str) -> Tuple[bool, str]:
"""
安装插件
"""
pass
if not pid or not repo_url:
return False, "参数错误"
# 从Github的repo_url获取用户和项目名
try:
user, repo = repo_url.split("/")[-4:-2]
except Exception as e:
return False, f"不支持的插件仓库地址格式:{str(e)}"
if not user or not repo:
return False, "不支持的插件仓库地址格式"
if SystemUtils.is_frozen():
return False, "可执行文件模式下,只能安装本地插件"
# 获取插件的文件列表
"""
[
{
"name": "__init__.py",
"path": "plugins/autobackup/__init__.py",
"sha": "cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
"size": 12385,
"url": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/contents/plugins/autobackup/__init__.py?ref=main",
"html_url": "https://github.com/jxxghp/MoviePilot-Plugins/blob/main/plugins/autobackup/__init__.py",
"git_url": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/git/blobs/cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
"download_url": "https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/plugins/autobackup/__init__.py",
"type": "file",
"_links": {
"self": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/contents/plugins/autobackup/__init__.py?ref=main",
"git": "https://api.github.com/repos/jxxghp/MoviePilot-Plugins/git/blobs/cd10eba3f0355d61adeb35561cb26a0a36c15a6c",
"html": "https://github.com/jxxghp/MoviePilot-Plugins/blob/main/plugins/autobackup/__init__.py"
}
}
]
"""
file_api = f"https://api.github.com/repos/{user}/{repo}/contents/plugins/{pid.lower()}"
res = RequestUtils(proxies=settings.PROXY).get_res(file_api)
if not res or res.status_code != 200:
return False, f"连接仓库失败:{res.status_code} - {res.reason}"
ret_json = res.json()
if ret_json and ret_json[0].get("message") == "Not Found":
return False, "插件在仓库中不存在"
# 本地存在时先删除
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid.lower()
if plugin_dir.exists():
shutil.rmtree(plugin_dir)
# 下载所有文件
for item in ret_json:
if item.get("download_url"):
# 下载插件文件
res = RequestUtils(proxies=settings.PROXY).get_res(item["download_url"])
if not res or res.status_code != 200:
return False, f"下载文件 {item.get('name')} 失败:{res.status_code} - {res.reason}"
# 创建插件文件夹
file_path = Path(settings.ROOT_PATH) / "app" / item.get("path")
if not file_path.parent.exists():
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(res.text)
return True, ""

View File

@ -34,3 +34,9 @@ class Plugin(BaseModel):
state: Optional[bool] = False
# 是否有详情页面
has_page: Optional[bool] = False
# 是否有新版本
has_update: Optional[bool] = False
# 是否本地
is_local: Optional[bool] = False
# 仓库地址
repo_url: Optional[str] = None

View File

@ -688,3 +688,26 @@ class StringUtils:
break
return ''.join(common_prefix)
@staticmethod
def compare_version(v1: str, v2: str) -> int:
"""
比较两个版本号的大小v1 > v2时返回1v1 < v2时返回-1v1 = v2时返回0
"""
if not v1 or not v2:
return 0
v1 = v1.replace('v', '')
v2 = v2.replace('v', '')
v1 = [int(x) for x in v1.split('.')]
v2 = [int(x) for x in v2.split('.')]
for i in range(min(len(v1), len(v2))):
if v1[i] > v2[i]:
return 1
elif v1[i] < v2[i]:
return -1
if len(v1) > len(v2):
return 1
elif len(v1) < len(v2):
return -1
else:
return 0