mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
Vendored
+57
-135
@@ -30,10 +30,8 @@ from requests import Response
|
||||
|
||||
from app.runtime.cache import cached, is_fresh
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.adapters.system.package import PackageInstallRequest, build_package_install_strategies
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
|
||||
from app.foundation.singleton import WeakSingleton
|
||||
|
||||
@@ -59,6 +57,24 @@ VERSION_BACKWARD_COMPATIBLE_FLAGS: Dict[str, List[str]] = {
|
||||
"v3": ["v2"],
|
||||
}
|
||||
|
||||
InstalledPluginsProvider = Callable[[], List[str]]
|
||||
|
||||
|
||||
def _empty_installed_plugins() -> List[str]:
|
||||
"""组合根尚未注入配置读取器时返回空安装清单。"""
|
||||
return []
|
||||
|
||||
|
||||
_installed_plugins_provider: InstalledPluginsProvider = _empty_installed_plugins
|
||||
|
||||
|
||||
def configure_installed_plugins_provider(
|
||||
provider: InstalledPluginsProvider,
|
||||
) -> None:
|
||||
"""由启动组合层注入已安装插件读取器,避免市场适配器访问数据库。"""
|
||||
global _installed_plugins_provider
|
||||
_installed_plugins_provider = provider
|
||||
|
||||
|
||||
def normalize_plugin_market_repo_url(repo_url: str) -> Optional[str]:
|
||||
"""规范化插件仓库地址,便于跨来源合并去重。"""
|
||||
@@ -165,10 +181,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"sqlalchemy, starlette, uvicorn; from pydantic import BaseModel, Field"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
"""初始化插件仓库配置访问器。"""
|
||||
self.systemconfig = SystemConfigOper()
|
||||
|
||||
@staticmethod
|
||||
def is_local_repo_url(repo_url: Optional[str]) -> bool:
|
||||
"""
|
||||
@@ -1173,7 +1185,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
try:
|
||||
install_plugins = {
|
||||
plugin_id.lower()
|
||||
for plugin_id in self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
for plugin_id in _installed_plugins_provider() or []
|
||||
}
|
||||
for plugin_id in install_plugins:
|
||||
wheels_dir = PLUGIN_DIR / plugin_id / "wheels"
|
||||
@@ -2099,68 +2111,26 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return False, f"解压 Release 压缩包失败:{e}"
|
||||
|
||||
def find_missing_dependencies(self) -> List[str]:
|
||||
"""
|
||||
收集所有需要安装或更新的依赖项
|
||||
1. 收集所有插件的依赖项,合并版本约束
|
||||
2. 获取已安装的包及其版本
|
||||
3. 比较已安装的包与所需的依赖项,找出需要安装或升级的包
|
||||
:return: 需要安装或更新的依赖项列表,例如 ["package1>=1.0.0", "package2"]
|
||||
"""
|
||||
try:
|
||||
# 收集所有插件的依赖项
|
||||
plugin_dependencies = self.__find_plugin_dependencies() # 返回格式为 {package_name: version_specifier}
|
||||
# 获取已安装的包及其版本
|
||||
installed_packages = self.__get_installed_packages() # 返回格式为 {package_name: Version}
|
||||
# 需要安装或更新的依赖项列表
|
||||
dependencies_to_install = []
|
||||
for pkg_name, version_specifier in plugin_dependencies.items():
|
||||
spec_set = SpecifierSet(version_specifier)
|
||||
installed_version = installed_packages.get(pkg_name)
|
||||
if installed_version is None:
|
||||
# 包未安装,需要安装
|
||||
if version_specifier:
|
||||
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
|
||||
else:
|
||||
dependencies_to_install.append(pkg_name)
|
||||
elif not spec_set.contains(installed_version, prereleases=True):
|
||||
# 已安装的版本不满足版本约束,需要升级或降级
|
||||
if version_specifier:
|
||||
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
|
||||
else:
|
||||
dependencies_to_install.append(pkg_name)
|
||||
# 已安装的版本满足要求,无需操作
|
||||
return dependencies_to_install
|
||||
except Exception as e:
|
||||
logger.error(f"收集所有需要安装或更新的依赖项时发生错误:{e}")
|
||||
return []
|
||||
"""兼容旧市场入口,转发到独立依赖适配器。"""
|
||||
installer = importlib.import_module(
|
||||
"app.adapters.system.plugin.dependency"
|
||||
).PluginDependencyInstaller
|
||||
return installer(
|
||||
self,
|
||||
installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).find_missing()
|
||||
|
||||
def install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
|
||||
"""
|
||||
安装指定的依赖项列表
|
||||
:param dependencies: 需要安装或更新的依赖项列表
|
||||
:return: (success, message)
|
||||
"""
|
||||
if not dependencies:
|
||||
return False, "没有传入需要安装的依赖项"
|
||||
|
||||
try:
|
||||
logger.debug(f"需要安装或更新的依赖项:{dependencies}")
|
||||
# 创建临时的 requirements.txt 文件用于批量安装
|
||||
requirements_temp_file = Path(settings.TEMP_PATH) / "plugin_dependencies" / "requirements.txt"
|
||||
requirements_temp_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(requirements_temp_file, "w", encoding="utf-8") as f:
|
||||
for dep in dependencies:
|
||||
f.write(dep + "\n")
|
||||
try:
|
||||
# 使用自动降级策略安装依赖
|
||||
wheels_dirs = self.__collect_plugin_wheels_dirs()
|
||||
return self.pip_install_with_fallback(requirements_temp_file, wheels_dirs)
|
||||
finally:
|
||||
# 删除临时文件
|
||||
requirements_temp_file.unlink()
|
||||
except Exception as e:
|
||||
logger.error(f"安装依赖项时发生错误:{e}")
|
||||
return False, f"安装依赖项时发生错误:{e}"
|
||||
"""兼容旧市场入口,转发到独立依赖适配器。"""
|
||||
installer = importlib.import_module(
|
||||
"app.adapters.system.plugin.dependency"
|
||||
).PluginDependencyInstaller
|
||||
return installer(
|
||||
self,
|
||||
installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).install(dependencies)
|
||||
|
||||
@classmethod
|
||||
def __get_installed_packages(cls) -> Dict[str, Version]:
|
||||
@@ -2203,9 +2173,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
try:
|
||||
install_plugins = {
|
||||
plugin_id.lower() # 对应插件的小写目录名
|
||||
for plugin_id in SystemConfigOper().get(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or []
|
||||
for plugin_id in _installed_plugins_provider() or []
|
||||
}
|
||||
for plugin_dir in PLUGIN_DIR.iterdir():
|
||||
if plugin_dir.is_dir():
|
||||
@@ -2739,34 +2707,15 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return False, False, "不存在依赖"
|
||||
|
||||
async def async_install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步安装指定的依赖项列表
|
||||
:param dependencies: 需要安装或更新的依赖项列表
|
||||
:return: (success, message)
|
||||
"""
|
||||
if not dependencies:
|
||||
return False, "没有传入需要安装的依赖项"
|
||||
|
||||
try:
|
||||
logger.debug(f"需要安装或更新的依赖项:{dependencies}")
|
||||
# 创建临时的 requirements.txt 文件用于批量安装
|
||||
requirements_temp_file = AsyncPath(settings.TEMP_PATH) / "plugin_dependencies" / "requirements.txt"
|
||||
await requirements_temp_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async with aiofiles.open(requirements_temp_file, "w", encoding="utf-8") as f:
|
||||
for dep in dependencies:
|
||||
await f.write(dep + "\n")
|
||||
|
||||
try:
|
||||
# 使用自动降级策略安装依赖
|
||||
wheels_dirs = self.__collect_plugin_wheels_dirs()
|
||||
return await self.__async_pip_install_with_fallback(Path(requirements_temp_file), wheels_dirs)
|
||||
finally:
|
||||
# 删除临时文件
|
||||
await requirements_temp_file.unlink()
|
||||
except Exception as e:
|
||||
logger.error(f"安装依赖项时发生错误:{e}")
|
||||
return False, f"安装依赖项时发生错误:{e}"
|
||||
"""兼容旧异步市场入口,转发到独立依赖适配器。"""
|
||||
installer = importlib.import_module(
|
||||
"app.adapters.system.plugin.dependency"
|
||||
).PluginDependencyInstaller
|
||||
return await installer(
|
||||
self,
|
||||
installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).async_install(dependencies)
|
||||
|
||||
async def __async_find_plugin_dependencies(self) -> Dict[str, str]:
|
||||
"""
|
||||
@@ -2779,9 +2728,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
try:
|
||||
install_plugins = {
|
||||
plugin_id.lower() # 对应插件的小写目录名
|
||||
for plugin_id in SystemConfigOper().get(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or []
|
||||
for plugin_id in _installed_plugins_provider() or []
|
||||
}
|
||||
|
||||
plugin_dir_path = AsyncPath(PLUGIN_DIR)
|
||||
@@ -2838,40 +2785,15 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return {}
|
||||
|
||||
async def async_find_missing_dependencies(self) -> List[str]:
|
||||
"""
|
||||
异步收集所有需要安装或更新的依赖项
|
||||
1. 收集所有插件的依赖项,合并版本约束
|
||||
2. 获取已安装的包及其版本
|
||||
3. 比较已安装的包与所需的依赖项,找出需要安装或升级的包
|
||||
:return: 需要安装或更新的依赖项列表,例如 ["package1>=1.0.0", "package2"]
|
||||
"""
|
||||
try:
|
||||
# 收集所有插件的依赖项
|
||||
plugin_dependencies = await self.__async_find_plugin_dependencies() # 返回格式为 {package_name: version_specifier}
|
||||
# 获取已安装的包及其版本
|
||||
installed_packages = self.__get_installed_packages() # 返回格式为 {package_name: Version}
|
||||
# 需要安装或更新的依赖项列表
|
||||
dependencies_to_install = []
|
||||
for pkg_name, version_specifier in plugin_dependencies.items():
|
||||
spec_set = SpecifierSet(version_specifier)
|
||||
installed_version = installed_packages.get(pkg_name)
|
||||
if installed_version is None:
|
||||
# 包未安装,需要安装
|
||||
if version_specifier:
|
||||
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
|
||||
else:
|
||||
dependencies_to_install.append(pkg_name)
|
||||
elif not spec_set.contains(installed_version, prereleases=True):
|
||||
# 已安装的版本不满足版本约束,需要升级或降级
|
||||
if version_specifier:
|
||||
dependencies_to_install.append(f"{pkg_name}{version_specifier}")
|
||||
else:
|
||||
dependencies_to_install.append(pkg_name)
|
||||
# 已安装的版本满足要求,无需操作
|
||||
return dependencies_to_install
|
||||
except Exception as e:
|
||||
logger.error(f"收集所有需要安装或更新的依赖项时发生错误:{e}")
|
||||
return []
|
||||
"""兼容旧异步市场入口,转发到独立依赖适配器。"""
|
||||
installer = importlib.import_module(
|
||||
"app.adapters.system.plugin.dependency"
|
||||
).PluginDependencyInstaller
|
||||
return await installer(
|
||||
self,
|
||||
installed_plugins_provider=_installed_plugins_provider,
|
||||
plugin_dir=PLUGIN_DIR,
|
||||
).async_find_missing()
|
||||
|
||||
async def async_install(self, pid: str, repo_url: str, package_version: Optional[str] = None,
|
||||
release_version: Optional[str] = None,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
"""插件市场外部适配器。"""
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
"""插件市场查询客户端。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.adapters.external.market import PluginHelper as _PluginHelper
|
||||
from app.runtime.cache import async_fresh, fresh
|
||||
|
||||
|
||||
class PluginMarketClient:
|
||||
"""把插件市场、版本元数据和本地仓库查询隔离为只读客户端。"""
|
||||
|
||||
def __init__(self, helper: Optional[_PluginHelper] = None) -> None:
|
||||
"""复用旧 PluginHelper 实现,保持缓存和弱单例身份不变。"""
|
||||
self._helper = helper or _PluginHelper()
|
||||
|
||||
def get_plugins(
|
||||
self,
|
||||
repo_url: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> Optional[dict[str, dict]]:
|
||||
"""同步读取指定仓库和代际的插件索引。"""
|
||||
with fresh(force):
|
||||
return self._helper.get_plugins(repo_url, package_version)
|
||||
|
||||
async def async_get_plugins(
|
||||
self,
|
||||
repo_url: str,
|
||||
package_version: Optional[str] = None,
|
||||
force: bool = False,
|
||||
) -> Optional[dict[str, dict]]:
|
||||
"""异步读取指定仓库和代际的插件索引。"""
|
||||
async with async_fresh(force):
|
||||
return await self._helper.async_get_plugins(repo_url, package_version)
|
||||
|
||||
def get_local_candidates(self) -> dict[str, dict]:
|
||||
"""返回全部本地插件仓库候选。"""
|
||||
return self._helper.get_local_plugin_candidates()
|
||||
|
||||
def get_local_candidate(
|
||||
self,
|
||||
plugin_id: str,
|
||||
package_version: Optional[str] = None,
|
||||
repo_path: Optional[Path] = None,
|
||||
**kwargs: Any,
|
||||
) -> Optional[dict]:
|
||||
"""返回指定插件的本地仓库候选。"""
|
||||
return self._helper.get_local_plugin_candidate(
|
||||
pid=plugin_id,
|
||||
package_version=package_version,
|
||||
repo_path=repo_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_local_repo_paths() -> list[Path]:
|
||||
"""返回配置中有效的本地插件仓库目录。"""
|
||||
return _PluginHelper.get_local_repo_paths()
|
||||
|
||||
@staticmethod
|
||||
def make_local_repo_url(
|
||||
plugin_id: str,
|
||||
repo_path: Optional[object] = None,
|
||||
package_version: Optional[str] = None,
|
||||
) -> str:
|
||||
"""生成兼容旧入口的本地插件来源标识。"""
|
||||
return _PluginHelper.make_local_repo_url(
|
||||
plugin_id,
|
||||
repo_path,
|
||||
package_version,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_local_repo_url(repo_url: Optional[str]) -> bool:
|
||||
"""判断插件来源是否为本地仓库标识。"""
|
||||
return _PluginHelper.is_local_repo_url(repo_url)
|
||||
|
||||
@staticmethod
|
||||
def annotate_system_version(plugin_info: dict) -> dict:
|
||||
"""补充插件所需 MoviePilot 版本兼容状态。"""
|
||||
return _PluginHelper.annotate_plugin_system_version(plugin_info)
|
||||
|
||||
@staticmethod
|
||||
def is_package_compatible(
|
||||
plugin_info: dict,
|
||||
package_version: Optional[str],
|
||||
) -> bool:
|
||||
"""判断插件条目是否兼容目标插件包代际。"""
|
||||
return _PluginHelper.is_package_plugin_compatible(
|
||||
plugin_info,
|
||||
package_version,
|
||||
)
|
||||
Vendored
+85
-171
@@ -9,9 +9,6 @@ from app.runtime.cache import cached
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import (
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
@@ -26,6 +23,21 @@ from app.adapters.system.host import SystemUtils
|
||||
from version import APP_VERSION, FRONTEND_VERSION
|
||||
|
||||
|
||||
_server_report_service: Any = None
|
||||
_server_sharing_service: Any = None
|
||||
|
||||
|
||||
def configure_server_application_services(
|
||||
*,
|
||||
report_service: Any,
|
||||
sharing_service: Any,
|
||||
) -> None:
|
||||
"""由启动组合根注入分享和存量上报应用服务。"""
|
||||
global _server_report_service, _server_sharing_service
|
||||
_server_report_service = report_service
|
||||
_server_sharing_service = sharing_service
|
||||
|
||||
|
||||
class MoviePilotServerHelper:
|
||||
"""
|
||||
MoviePilot 服务端请求辅助工具。
|
||||
@@ -51,22 +63,23 @@ class MoviePilotServerHelper:
|
||||
_RECOGNIZE_SHARE_PATH = "/recognize/share"
|
||||
_USER_PERMISSIONS_PATH = "/user/permissions"
|
||||
_LOCAL_REPO_PREFIX = "local://"
|
||||
_SUBSCRIBE_STATISTIC_FIELDS = frozenset({
|
||||
"name", "year", "type", "media_source", "media_id", "music_type",
|
||||
"total_tracks", "genre_ids", "season", "poster", "backdrop", "vote",
|
||||
"description",
|
||||
})
|
||||
_SUBSCRIBE_SHARE_FIELDS = frozenset({
|
||||
"share_title", "share_comment", "share_user", "share_uid", "name",
|
||||
"year", "type", "keyword", "media_source", "media_id", "music_type",
|
||||
"total_tracks", "season", "poster", "backdrop", "vote", "description",
|
||||
"genre_ids", "include", "exclude", "quality", "resolution", "effect",
|
||||
"total_episode", "custom_words", "media_category", "episode_group",
|
||||
"date",
|
||||
})
|
||||
_user_uid: Optional[str] = None
|
||||
_github_user: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def _report_service(cls) -> Any:
|
||||
"""返回启动组合根注入的存量上报应用服务。"""
|
||||
if _server_report_service is None:
|
||||
raise RuntimeError("中心服务上报用例尚未由启动组合根装配")
|
||||
return _server_report_service
|
||||
|
||||
@classmethod
|
||||
def _sharing_service(cls) -> Any:
|
||||
"""返回启动组合根注入的订阅和工作流分享应用服务。"""
|
||||
if _server_sharing_service is None:
|
||||
raise RuntimeError("中心服务分享用例尚未由启动组合根装配")
|
||||
return _server_sharing_service
|
||||
|
||||
@classmethod
|
||||
def get_user_uid(cls) -> Optional[str]:
|
||||
"""
|
||||
@@ -334,22 +347,22 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
初始化订阅统计上报状态。
|
||||
"""
|
||||
systemconfig = SystemConfigOper()
|
||||
if settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
if not systemconfig.get(SystemConfigKey.SubscribeReport):
|
||||
if cls.sub_report():
|
||||
systemconfig.set(SystemConfigKey.SubscribeReport, "1")
|
||||
cls._report_service().init_report(
|
||||
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
|
||||
state_key=SystemConfigKey.SubscribeReport,
|
||||
reporter=cls.sub_report,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def init_plugin_report(cls) -> None:
|
||||
"""
|
||||
初始化插件安装统计上报状态。
|
||||
"""
|
||||
systemconfig = SystemConfigOper()
|
||||
if settings.PLUGIN_STATISTIC_SHARE:
|
||||
if not systemconfig.get(SystemConfigKey.PluginInstallReport):
|
||||
if cls.install_plugin_report():
|
||||
systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
|
||||
cls._report_service().init_report(
|
||||
enabled=settings.PLUGIN_STATISTIC_SHARE,
|
||||
state_key=SystemConfigKey.PluginInstallReport,
|
||||
reporter=cls.install_plugin_report,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_list_response(res) -> List[dict]:
|
||||
@@ -599,26 +612,20 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
批量上报存量插件安装统计。
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
payload_plugins = cls._build_plugin_report_payload(items)
|
||||
if not payload_plugins:
|
||||
return False
|
||||
res = cls.plugin_install_report(payload_plugins)
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
return cls._report_service().report_plugins(
|
||||
enabled=settings.PLUGIN_STATISTIC_SHARE,
|
||||
items=items,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def async_install_plugin_report(cls, items: Optional[List[Tuple[str, Optional[str]]]] = None) -> bool:
|
||||
"""
|
||||
异步批量上报存量插件安装统计。
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
payload_plugins = cls._build_plugin_report_payload(items)
|
||||
if not payload_plugins:
|
||||
return False
|
||||
res = await cls.async_plugin_install_report(payload_plugins)
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
return await cls._report_service().async_report_plugins(
|
||||
enabled=settings.PLUGIN_STATISTIC_SHARE,
|
||||
items=items,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def subscribe_statistic(cls, params: Dict[str, Any]):
|
||||
@@ -888,20 +895,9 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
上报存量订阅统计。
|
||||
"""
|
||||
if not settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
return False
|
||||
subscribes = SubscribeOper().list()
|
||||
if not subscribes:
|
||||
return True
|
||||
payloads = [
|
||||
payload
|
||||
for sub in subscribes
|
||||
if (payload := cls._build_subscribe_statistic_payload(sub.to_dict()))
|
||||
]
|
||||
if not payloads:
|
||||
return True
|
||||
res = cls.subscribe_report(payloads)
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
return cls._report_service().report_subscribes(
|
||||
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def sub_share(
|
||||
@@ -914,21 +910,13 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
分享订阅。
|
||||
"""
|
||||
if not settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
return False, "当前没有开启订阅数据共享功能"
|
||||
subscribe = SubscribeOper().get(subscribe_id)
|
||||
if not subscribe:
|
||||
return False, "订阅不存在"
|
||||
payload = cls._build_subscribe_share_payload({
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": cls.get_user_uuid(),
|
||||
**subscribe.to_dict(),
|
||||
})
|
||||
if not payload:
|
||||
return False, "订阅媒体身份不完整"
|
||||
return cls._handle_response(cls.subscribe_share(payload), cls._clear_subscribe_share_cache)
|
||||
return cls._sharing_service().share_subscribe(
|
||||
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
|
||||
subscribe_id=subscribe_id,
|
||||
share_title=share_title,
|
||||
share_comment=share_comment,
|
||||
share_user=share_user,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def async_sub_share(
|
||||
@@ -941,23 +929,12 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
异步分享订阅。
|
||||
"""
|
||||
if not settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
return False, "当前没有开启订阅数据共享功能"
|
||||
subscribe = await SubscribeOper().async_get(subscribe_id)
|
||||
if not subscribe:
|
||||
return False, "订阅不存在"
|
||||
payload = cls._build_subscribe_share_payload({
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": cls.get_user_uuid(),
|
||||
**subscribe.to_dict(),
|
||||
})
|
||||
if not payload:
|
||||
return False, "订阅媒体身份不完整"
|
||||
return cls._handle_response(
|
||||
await cls.async_subscribe_share(payload),
|
||||
cls._clear_subscribe_share_cache,
|
||||
return await cls._sharing_service().async_share_subscribe(
|
||||
enabled=settings.SUBSCRIBE_STATISTIC_SHARE,
|
||||
subscribe_id=subscribe_id,
|
||||
share_title=share_title,
|
||||
share_comment=share_comment,
|
||||
share_user=share_user,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -965,38 +942,14 @@ class MoviePilotServerHelper:
|
||||
cls, item: Optional[dict]
|
||||
) -> Optional[dict]:
|
||||
"""构造中心服务订阅统计载荷,只保留统一身份和公开统计字段。"""
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
media_source, media_id = resolve_media_identity(media=item)
|
||||
if not media_source or not media_id:
|
||||
return None
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in item.items()
|
||||
if key in cls._SUBSCRIBE_STATISTIC_FIELDS
|
||||
}
|
||||
payload["media_source"] = str(media_source)
|
||||
payload["media_id"] = media_id
|
||||
return payload
|
||||
return cls._report_service().build_subscribe_payload(item)
|
||||
|
||||
@classmethod
|
||||
def _build_subscribe_share_payload(
|
||||
cls, item: Optional[dict]
|
||||
) -> Optional[dict]:
|
||||
"""构造中心服务订阅分享载荷,隔离本地运行字段和旧专用 ID。"""
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
media_source, media_id = resolve_media_identity(media=item)
|
||||
if not media_source or not media_id:
|
||||
return None
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in item.items()
|
||||
if key in cls._SUBSCRIBE_SHARE_FIELDS
|
||||
}
|
||||
payload["media_source"] = str(media_source)
|
||||
payload["media_id"] = media_id
|
||||
return payload
|
||||
return cls._sharing_service().build_subscribe_payload(item)
|
||||
|
||||
@classmethod
|
||||
def share_delete(cls, share_id: int) -> Tuple[bool, str]:
|
||||
@@ -1180,17 +1133,12 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
return await cls._async_get(cls._server_url(cls._WORKFLOW_SHARES_PATH), params=params, timeout=15)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_workflow_data(workflow) -> dict:
|
||||
@classmethod
|
||||
def _prepare_workflow_data(cls, workflow) -> dict:
|
||||
"""
|
||||
准备工作流分享数据。
|
||||
"""
|
||||
workflow_dict = workflow.to_dict()
|
||||
workflow_dict.pop("id", None)
|
||||
workflow_dict.pop("context", None)
|
||||
workflow_dict["actions"] = json.dumps(workflow_dict["actions"] or [])
|
||||
workflow_dict["flows"] = json.dumps(workflow_dict["flows"] or [])
|
||||
return workflow_dict
|
||||
return cls._sharing_service().prepare_workflow(workflow)
|
||||
|
||||
@classmethod
|
||||
def workflow_share_by_id(
|
||||
@@ -1203,20 +1151,13 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
分享工作流。
|
||||
"""
|
||||
if not settings.WORKFLOW_STATISTIC_SHARE:
|
||||
return False, "当前没有开启工作流数据共享功能"
|
||||
workflow = WorkflowOper().get(workflow_id)
|
||||
valid, message = cls._validate_workflow(workflow)
|
||||
if not valid:
|
||||
return False, message
|
||||
payload = {
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": cls.get_user_uuid(),
|
||||
**cls._prepare_workflow_data(workflow),
|
||||
}
|
||||
return cls._handle_response(cls.workflow_share(payload), cls._clear_workflow_share_cache)
|
||||
return cls._sharing_service().share_workflow(
|
||||
enabled=settings.WORKFLOW_STATISTIC_SHARE,
|
||||
workflow_id=workflow_id,
|
||||
share_title=share_title,
|
||||
share_comment=share_comment,
|
||||
share_user=share_user,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def async_workflow_share_by_id(
|
||||
@@ -1229,22 +1170,12 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
异步分享工作流。
|
||||
"""
|
||||
if not settings.WORKFLOW_STATISTIC_SHARE:
|
||||
return False, "当前没有开启工作流数据共享功能"
|
||||
workflow = await WorkflowOper().async_get(workflow_id)
|
||||
valid, message = cls._validate_workflow(workflow)
|
||||
if not valid:
|
||||
return False, message
|
||||
payload = {
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": cls.get_user_uuid(),
|
||||
**cls._prepare_workflow_data(workflow),
|
||||
}
|
||||
return cls._handle_response(
|
||||
await cls.async_workflow_share(payload),
|
||||
cls._clear_workflow_share_cache,
|
||||
return await cls._sharing_service().async_share_workflow(
|
||||
enabled=settings.WORKFLOW_STATISTIC_SHARE,
|
||||
workflow_id=workflow_id,
|
||||
share_title=share_title,
|
||||
share_comment=share_comment,
|
||||
share_user=share_user,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -1327,16 +1258,12 @@ class MoviePilotServerHelper:
|
||||
"count": count,
|
||||
}))
|
||||
|
||||
@staticmethod
|
||||
def _validate_workflow(workflow) -> Tuple[bool, str]:
|
||||
@classmethod
|
||||
def _validate_workflow(cls, workflow) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证工作流是否可以分享。
|
||||
"""
|
||||
if not workflow:
|
||||
return False, "工作流不存在"
|
||||
if not workflow.actions or not workflow.flows:
|
||||
return False, "请分享有动作和流程的工作流"
|
||||
return True, ""
|
||||
return cls._sharing_service().validate_workflow(workflow)
|
||||
|
||||
@classmethod
|
||||
def recognize_share_url(cls) -> Optional[str]:
|
||||
@@ -1754,20 +1681,7 @@ class MoviePilotServerHelper:
|
||||
"""
|
||||
构建批量插件安装统计载荷。
|
||||
"""
|
||||
if items:
|
||||
return [
|
||||
{
|
||||
"plugin_id": plugin_id,
|
||||
"repo_url": cls.sanitize_plugin_repo_url(repo_url),
|
||||
}
|
||||
for plugin_id, repo_url in items
|
||||
if plugin_id
|
||||
]
|
||||
|
||||
plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins)
|
||||
if not plugins:
|
||||
return []
|
||||
return [{"plugin_id": plugin, "repo_url": None} for plugin in plugins]
|
||||
return cls._report_service().build_plugin_payload(items)
|
||||
|
||||
@classmethod
|
||||
def _parse_local_repo_plugin_id(cls, repo_url: str) -> Optional[str]:
|
||||
|
||||
Reference in New Issue
Block a user