feat 在线仓库插件安装
This commit is contained in:
parent
0dac3f1b1d
commit
fbe306ba90
@ -97,7 +97,6 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **❗COOKIECLOUD_PASSWORD:** CookieCloud端对端加密密码
|
||||
- **❗COOKIECLOUD_INTERVAL:** CookieCloud同步间隔(分钟)
|
||||
- **❗USER_AGENT:** CookieCloud保存Cookie对应的浏览器UA,建议配置,设置后可增加连接站点的成功率,同步站点后可以在管理界面中修改
|
||||
- **OCR_HOST:** OCR识别服务器地址,格式:`http(s)://ip:port`,用于识别站点验证码实现自动登录获取Cookie等,不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
|
||||
---
|
||||
- **SUBSCRIBE_MODE:** 订阅模式,`rss`/`spider`,默认`spider`,`rss`模式通过定时刷新RSS来匹配订阅(RSS地址会自动获取,也可手动维护),对站点压力小,同时可设置订阅刷新周期,24小时运行,但订阅和下载通知不能过滤和显示免费,推荐使用rss模式。
|
||||
- **SUBSCRIBE_RSS_INTERVAL:** RSS订阅模式刷新时间间隔(分钟),默认`30`分钟,不能小于5分钟。
|
||||
@ -106,6 +105,9 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
---
|
||||
- **AUTO_DOWNLOAD_USER:** 远程交互搜索时自动择优下载的用户ID,多个用户使用,分割,未设置需要选择资源或者回复`0`
|
||||
- **❗MESSAGER:** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
|
||||
---
|
||||
- **OCR_HOST:** OCR识别服务器地址,格式:`http(s)://ip:port`,用于识别站点验证码实现自动登录获取Cookie等,不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
|
||||
- **PLUGIN_MARKET:** 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/,默认为官方插件仓库:`https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/`。
|
||||
|
||||
- `wechat`设置项:
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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, ""
|
||||
|
@ -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
|
||||
|
@ -688,3 +688,26 @@ class StringUtils:
|
||||
break
|
||||
|
||||
return ''.join(common_prefix)
|
||||
|
||||
@staticmethod
|
||||
def compare_version(v1: str, v2: str) -> int:
|
||||
"""
|
||||
比较两个版本号的大小,v1 > v2时返回1,v1 < v2时返回-1,v1 = 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user