mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-27 11:11:07 +08:00
refactor: centralize MoviePilot server helper
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import Any, Optional
|
||||
from app.core.config import settings
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.plugin import PluginHelper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
@@ -230,7 +231,7 @@ async def install_plugin_runtime(
|
||||
refreshed_only = False
|
||||
if not force and plugin_id in plugin_manager.get_plugin_ids():
|
||||
refreshed_only = True
|
||||
await plugin_helper.async_install_reg(pid=plugin_id, repo_url=repo_url)
|
||||
await MoviePilotServerHelper.async_install_plugin_reg(plugin_id=plugin_id, repo_url=repo_url)
|
||||
message = "插件已存在,已刷新加载"
|
||||
else:
|
||||
if not repo_url:
|
||||
@@ -242,6 +243,7 @@ async def install_plugin_runtime(
|
||||
)
|
||||
if not state:
|
||||
return False, message, False
|
||||
await MoviePilotServerHelper.async_install_plugin_reg(plugin_id=plugin_id, repo_url=repo_url)
|
||||
|
||||
if plugin_id not in install_plugins:
|
||||
install_plugins.append(plugin_id)
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.core.event import eventmanager
|
||||
from app.db.subscribe_oper import SubscribeOper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
@@ -49,7 +49,7 @@ class DeleteSubscribeTool(MoviePilotTool):
|
||||
|
||||
await subscribe_oper.async_delete(subscribe_id)
|
||||
# 分享订阅统计刷新本身已异步化,这里只需要在删除后触发即可。
|
||||
SubscribeHelper().sub_done_async(
|
||||
MoviePilotServerHelper.sub_done_async(
|
||||
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.core.context import MediaInfo
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType, media_type_to_agent
|
||||
|
||||
@@ -77,8 +77,7 @@ class QueryPopularSubscribesTool(MoviePilotTool):
|
||||
if not media_type_enum:
|
||||
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
|
||||
|
||||
subscribe_helper = SubscribeHelper()
|
||||
subscribes = await subscribe_helper.async_get_statistic(
|
||||
subscribes = await MoviePilotServerHelper.async_get_subscribe_statistic(
|
||||
stype=media_type_enum.to_agent(),
|
||||
page=page,
|
||||
count=count,
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Optional, Type
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.log import logger
|
||||
|
||||
MAX_PAGE_SIZE = 50
|
||||
@@ -68,8 +68,7 @@ class QuerySubscribeSharesTool(MoviePilotTool):
|
||||
# 订阅分享是外部列表型结果,限制单页大小能降低工具上下文占用。
|
||||
count = min(count, MAX_PAGE_SIZE)
|
||||
|
||||
subscribe_helper = SubscribeHelper()
|
||||
shares = await subscribe_helper.async_get_shares(
|
||||
shares = await MoviePilotServerHelper.async_get_subscribe_shares(
|
||||
name=name,
|
||||
page=page,
|
||||
count=count,
|
||||
|
||||
@@ -21,6 +21,7 @@ from app.db.user_oper import (
|
||||
get_current_active_superuser_async,
|
||||
)
|
||||
from app.factory import app
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.plugin import PluginHelper
|
||||
from app.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
@@ -217,7 +218,7 @@ async def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
插件安装统计
|
||||
"""
|
||||
return await PluginHelper().async_get_statistic()
|
||||
return await MoviePilotServerHelper.async_get_plugin_statistic()
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -257,7 +258,7 @@ async def install(
|
||||
)
|
||||
if compatible_message:
|
||||
return schemas.Response(success=False, message=compatible_message)
|
||||
await plugin_helper.async_install_reg(pid=plugin_id, repo_url=repo_url)
|
||||
await MoviePilotServerHelper.async_install_plugin_reg(plugin_id=plugin_id, repo_url=repo_url)
|
||||
else:
|
||||
# 插件不存在或需要强制安装,下载安装并注册插件
|
||||
if repo_url:
|
||||
@@ -267,6 +268,7 @@ async def install(
|
||||
# 安装失败则直接响应
|
||||
if not state:
|
||||
return schemas.Response(success=False, message=msg)
|
||||
await MoviePilotServerHelper.async_install_plugin_reg(plugin_id=plugin_id, repo_url=repo_url)
|
||||
else:
|
||||
# repo_url 为空时,也直接响应
|
||||
return schemas.Response(
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.db.models.subscribehistory import SubscribeHistory
|
||||
from app.db.models.user import User
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_user_async
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import MediaType, EventType, SystemConfigKey
|
||||
|
||||
@@ -491,7 +491,7 @@ async def popular_subscribes(
|
||||
"""
|
||||
查询热门订阅
|
||||
"""
|
||||
subscribes = await SubscribeHelper().async_get_statistic(
|
||||
subscribes = await MoviePilotServerHelper.async_get_subscribe_statistic(
|
||||
stype=stype,
|
||||
page=page,
|
||||
count=count,
|
||||
@@ -574,7 +574,7 @@ async def subscribe_share(
|
||||
"""
|
||||
分享订阅
|
||||
"""
|
||||
state, errmsg = await SubscribeHelper().async_sub_share(
|
||||
state, errmsg = await MoviePilotServerHelper.async_sub_share(
|
||||
subscribe_id=sub.subscribe_id,
|
||||
share_title=sub.share_title,
|
||||
share_comment=sub.share_comment,
|
||||
@@ -590,7 +590,7 @@ async def subscribe_share_delete(
|
||||
"""
|
||||
删除分享
|
||||
"""
|
||||
state, errmsg = await SubscribeHelper().async_share_delete(share_id=share_id)
|
||||
state, errmsg = await MoviePilotServerHelper.async_share_delete(share_id=share_id)
|
||||
return schemas.Response(success=state, message=errmsg)
|
||||
|
||||
|
||||
@@ -611,7 +611,7 @@ async def subscribe_fork(
|
||||
subscribe_in=schemas.Subscribe(**sub_dict), current_user=current_user
|
||||
)
|
||||
if result.success:
|
||||
await SubscribeHelper().async_sub_fork(share_id=sub.id)
|
||||
await MoviePilotServerHelper.async_sub_fork(share_id=sub.id)
|
||||
return result
|
||||
|
||||
|
||||
@@ -673,7 +673,7 @@ async def subscribe_shares(
|
||||
"""
|
||||
查询分享的订阅
|
||||
"""
|
||||
return await SubscribeHelper().async_get_shares(
|
||||
return await MoviePilotServerHelper.async_get_subscribe_shares(
|
||||
name=name,
|
||||
page=page,
|
||||
count=count,
|
||||
@@ -696,7 +696,7 @@ async def subscribe_share_statistics(
|
||||
查询订阅分享统计
|
||||
返回每个分享人分享的媒体数量以及总的复用人次
|
||||
"""
|
||||
return await SubscribeHelper().async_get_share_statistics()
|
||||
return await MoviePilotServerHelper.async_get_subscribe_share_statistics()
|
||||
|
||||
|
||||
@router.get("/{subscribe_id}", summary="订阅详情", response_model=schemas.Subscribe)
|
||||
@@ -733,7 +733,7 @@ async def delete_subscribe(
|
||||
{"subscribe_id": subscribe_id, "subscribe_info": subscribe_info},
|
||||
)
|
||||
# 统计订阅
|
||||
SubscribeHelper().sub_done_async(
|
||||
MoviePilotServerHelper.sub_done_async(
|
||||
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
||||
)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -31,11 +31,10 @@ from app.db.user_oper import (
|
||||
)
|
||||
from app.helper.image import ImageHelper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.progress import ProgressHelper
|
||||
from app.helper.rule import RuleHelper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.system import SystemHelper
|
||||
from app.helper.usage import UsageHelper
|
||||
from app.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas import ConfigChangeEventData
|
||||
@@ -492,10 +491,10 @@ async def get_user_global_setting(_: User = Depends(get_current_active_user_asyn
|
||||
info["LLM_SUPPORT_AUDIO_OUTPUT"] = False
|
||||
|
||||
# 追加用户唯一ID和订阅分享管理权限
|
||||
share_admin = SubscribeHelper().is_admin_user()
|
||||
share_admin = MoviePilotServerHelper.is_admin_user()
|
||||
info.update(
|
||||
{
|
||||
"USER_UNIQUE_ID": SubscribeHelper().get_user_uuid(),
|
||||
"USER_UNIQUE_ID": MoviePilotServerHelper.get_user_uuid(),
|
||||
"SUBSCRIBE_SHARE_MANAGE": share_admin,
|
||||
"WORKFLOW_SHARE_MANAGE": share_admin,
|
||||
}
|
||||
@@ -527,7 +526,7 @@ async def usage_statistic(_: User = Depends(get_current_active_user_async)):
|
||||
"""
|
||||
查询安装版本统计报表
|
||||
"""
|
||||
return schemas.Response(success=True, data=await UsageHelper().async_get_statistic())
|
||||
return schemas.Response(success=True, data=await MoviePilotServerHelper.async_get_usage_statistic())
|
||||
|
||||
|
||||
@router.post("/env", summary="更新系统配置", response_model=schemas.Response)
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.db import get_async_db, get_db
|
||||
from app.db.models import Workflow
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.workflow_oper import WorkflowOper
|
||||
from app.helper.workflow import WorkflowHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import EventType, EVENT_TYPE_NAMES
|
||||
|
||||
@@ -100,7 +100,7 @@ async def workflow_share(
|
||||
success=False, message="请填写工作流ID、分享标题和分享人"
|
||||
)
|
||||
|
||||
state, errmsg = await WorkflowHelper().async_workflow_share(
|
||||
state, errmsg = await MoviePilotServerHelper.async_workflow_share_by_id(
|
||||
workflow_id=workflow.id,
|
||||
share_title=workflow.share_title or "",
|
||||
share_comment=workflow.share_comment or "",
|
||||
@@ -116,7 +116,7 @@ async def workflow_share_delete(
|
||||
"""
|
||||
删除分享
|
||||
"""
|
||||
state, errmsg = await WorkflowHelper().async_share_delete(share_id=share_id)
|
||||
state, errmsg = await MoviePilotServerHelper.async_workflow_share_delete_by_id(share_id=share_id)
|
||||
return schemas.Response(success=state, message=errmsg)
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ async def workflow_fork(
|
||||
|
||||
# 更新复用次数
|
||||
if workflow:
|
||||
await WorkflowHelper().async_workflow_fork(share_id=workflow.id)
|
||||
await MoviePilotServerHelper.async_workflow_fork_by_id(share_id=workflow.id)
|
||||
|
||||
return schemas.Response(success=True, message="复用成功")
|
||||
|
||||
@@ -191,7 +191,7 @@ async def workflow_shares(
|
||||
"""
|
||||
查询分享的工作流
|
||||
"""
|
||||
return await WorkflowHelper().async_get_shares(name=name, page=page, count=count)
|
||||
return await MoviePilotServerHelper.async_get_workflow_shares(name=name, page=page, count=count)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -21,8 +21,8 @@ from app.core.module import ModuleManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db.message_oper import MessageOper
|
||||
from app.db.user_oper import UserOper
|
||||
from app.helper.recognize import MediaRecognizeShareHelper
|
||||
from app.helper.message import MessageHelper, MessageQueueManager, MessageTemplateHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.service import ServiceConfigHelper
|
||||
from app.log import logger
|
||||
from app.schemas import (
|
||||
@@ -591,7 +591,6 @@ class ChainBase(metaclass=ABCMeta):
|
||||
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||
mtype = meta.type
|
||||
share_query_meta = share_meta or meta
|
||||
share_helper = MediaRecognizeShareHelper()
|
||||
with fresh(not cache):
|
||||
mediainfo = self.run_module(
|
||||
"recognize_media",
|
||||
@@ -605,7 +604,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
)
|
||||
if mediainfo:
|
||||
if not mediainfo.recognize_cache_hit:
|
||||
share_helper.report(
|
||||
MoviePilotServerHelper.report_recognize_share(
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
keyword_meta=share_query_meta,
|
||||
@@ -616,12 +615,12 @@ class ChainBase(metaclass=ABCMeta):
|
||||
share_query_meta, tmdbid, doubanid, bangumiid
|
||||
):
|
||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
||||
shared_item = share_helper.query(
|
||||
shared_item = MoviePilotServerHelper.query_recognize_share(
|
||||
meta=meta,
|
||||
mtype=mtype,
|
||||
keyword_meta=share_query_meta,
|
||||
)
|
||||
shared_params = share_helper.to_recognize_params(shared_item)
|
||||
shared_params = MoviePilotServerHelper.to_recognize_params(shared_item)
|
||||
if shared_params:
|
||||
with fresh(not cache):
|
||||
mediainfo = self.run_module(
|
||||
@@ -676,7 +675,6 @@ class ChainBase(metaclass=ABCMeta):
|
||||
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||
mtype = meta.type
|
||||
share_query_meta = share_meta or meta
|
||||
share_helper = MediaRecognizeShareHelper()
|
||||
async with async_fresh(not cache):
|
||||
mediainfo = await self.async_run_module(
|
||||
"async_recognize_media",
|
||||
@@ -690,7 +688,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
)
|
||||
if mediainfo:
|
||||
if not mediainfo.recognize_cache_hit:
|
||||
await share_helper.async_report(
|
||||
await MoviePilotServerHelper.async_report_recognize_share(
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
keyword_meta=share_query_meta,
|
||||
@@ -701,12 +699,12 @@ class ChainBase(metaclass=ABCMeta):
|
||||
share_query_meta, tmdbid, doubanid, bangumiid
|
||||
):
|
||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
||||
shared_item = await share_helper.async_query(
|
||||
shared_item = await MoviePilotServerHelper.async_query_recognize_share(
|
||||
meta=meta,
|
||||
mtype=mtype,
|
||||
keyword_meta=share_query_meta,
|
||||
)
|
||||
shared_params = share_helper.to_recognize_params(shared_item)
|
||||
shared_params = MoviePilotServerHelper.to_recognize_params(shared_item)
|
||||
if shared_params:
|
||||
async with async_fresh(not cache):
|
||||
mediainfo = await self.async_run_module(
|
||||
|
||||
@@ -34,7 +34,7 @@ from app.db.models.subscribe import Subscribe
|
||||
from app.db.site_oper import SiteOper
|
||||
from app.db.subscribe_oper import SubscribeOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaRecognizeConvertEventData
|
||||
@@ -709,7 +709,7 @@ class SubscribeChain(ChainBase):
|
||||
"mediainfo": mediainfo.to_dict(),
|
||||
})
|
||||
# 统计订阅
|
||||
SubscribeHelper().sub_reg_async({
|
||||
MoviePilotServerHelper.sub_reg_async({
|
||||
"name": title,
|
||||
"year": year,
|
||||
"type": metainfo.type.value,
|
||||
@@ -890,7 +890,7 @@ class SubscribeChain(ChainBase):
|
||||
"mediainfo": mediainfo.to_dict(),
|
||||
})
|
||||
# 统计订阅
|
||||
await SubscribeHelper().async_sub_reg({
|
||||
await MoviePilotServerHelper.async_sub_reg({
|
||||
"name": title,
|
||||
"year": year,
|
||||
"type": metainfo.type.value,
|
||||
@@ -1752,7 +1752,7 @@ class SubscribeChain(ChainBase):
|
||||
logger.info(f'开始刷新follow用户分享订阅 ...')
|
||||
success_count = 0
|
||||
subscribeoper = SubscribeOper()
|
||||
for share_sub in SubscribeHelper().get_shares():
|
||||
for share_sub in MoviePilotServerHelper.get_subscribe_shares():
|
||||
if global_vars.is_system_stopped:
|
||||
break
|
||||
uid = share_sub.get("share_uid")
|
||||
@@ -2024,7 +2024,7 @@ class SubscribeChain(ChainBase):
|
||||
"mediainfo": mediainfo.to_dict(),
|
||||
})
|
||||
# 统计订阅
|
||||
SubscribeHelper().sub_done_async({
|
||||
MoviePilotServerHelper.sub_done_async({
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"doubanid": mediainfo.douban_id
|
||||
})
|
||||
@@ -2667,7 +2667,6 @@ class SubscribeChain(ChainBase):
|
||||
return False, "请输入至少一个有效的订阅 ID"
|
||||
|
||||
subscribeoper = SubscribeOper()
|
||||
subscribehelper = SubscribeHelper()
|
||||
deleted = []
|
||||
missing = []
|
||||
for subscribe_id in subscribe_ids:
|
||||
@@ -2677,7 +2676,7 @@ class SubscribeChain(ChainBase):
|
||||
continue
|
||||
deleted.append(subscribe.name)
|
||||
subscribeoper.delete(subscribe_id)
|
||||
subscribehelper.sub_done_async(
|
||||
MoviePilotServerHelper.sub_done_async(
|
||||
{
|
||||
"tmdbid": subscribe.tmdbid,
|
||||
"doubanid": subscribe.doubanid,
|
||||
@@ -2706,7 +2705,6 @@ class SubscribeChain(ChainBase):
|
||||
return
|
||||
arg_strs = str(arg_str).split()
|
||||
subscribeoper = SubscribeOper()
|
||||
subscribehelper = SubscribeHelper()
|
||||
for arg_str in arg_strs:
|
||||
arg_str = arg_str.strip()
|
||||
if not arg_str.isdigit():
|
||||
@@ -2720,7 +2718,7 @@ class SubscribeChain(ChainBase):
|
||||
# 删除订阅
|
||||
subscribeoper.delete(subscribe_id)
|
||||
# 统计订阅
|
||||
subscribehelper.sub_done_async({
|
||||
MoviePilotServerHelper.sub_done_async({
|
||||
"tmdbid": subscribe.tmdbid,
|
||||
"doubanid": subscribe.doubanid
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ from app.core.config import settings
|
||||
from app.core.event import eventmanager
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.plugin import PluginHelper
|
||||
from app.helper.sites import SitesHelper # noqa
|
||||
from app.log import logger
|
||||
@@ -591,6 +592,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
state, msg = PluginHelper().install(pid=plugin.id, repo_url=plugin.repo_url, force_install=True)
|
||||
elapsed_time = time.time() - start_time
|
||||
if state:
|
||||
MoviePilotServerHelper.install_plugin_reg(plugin_id=plugin.id, repo_url=plugin.repo_url)
|
||||
logger.info(
|
||||
f"插件 {plugin.plugin_name} 安装成功,版本:{plugin.plugin_version},耗时:{elapsed_time:.2f} 秒")
|
||||
sync_plugins.append(plugin.id)
|
||||
|
||||
@@ -49,9 +49,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
|
||||
_base_url = "https://raw.githubusercontent.com/{user}/{repo}/main/"
|
||||
_install_reg = f"{settings.MP_SERVER_HOST}/plugin/install/{{pid}}"
|
||||
_install_report = f"{settings.MP_SERVER_HOST}/plugin/install"
|
||||
_install_statistic = f"{settings.MP_SERVER_HOST}/plugin/statistic"
|
||||
# 串行化运行期依赖安装,避免多个 pip 子进程和导入缓存刷新互相踩踏。
|
||||
_pip_install_lock = threading.Lock()
|
||||
# 这些包一旦被插件覆盖,最容易直接拖垮主程序启动,因此冲突提示需要单独高亮。
|
||||
@@ -72,10 +69,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
|
||||
def __init__(self):
|
||||
self.systemconfig = SystemConfigOper()
|
||||
if settings.PLUGIN_STATISTIC_SHARE:
|
||||
if not self.systemconfig.get(SystemConfigKey.PluginInstallReport):
|
||||
if self.install_report():
|
||||
self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
|
||||
|
||||
@staticmethod
|
||||
def is_local_repo_url(repo_url: Optional[str]) -> bool:
|
||||
@@ -147,25 +140,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def sanitize_repo_url_for_statistic(repo_url: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
统计上报前脱敏 repo_url,避免泄露本地仓库绝对路径
|
||||
"""
|
||||
if not repo_url:
|
||||
return repo_url
|
||||
if not PluginHelper.is_local_repo_url(repo_url):
|
||||
return repo_url
|
||||
|
||||
pid = PluginHelper.parse_local_repo_url(repo_url)
|
||||
if not pid:
|
||||
return LOCAL_REPO_PREFIX.rstrip("/")
|
||||
|
||||
return PluginHelper.make_local_repo_url(
|
||||
pid=pid,
|
||||
package_version=PluginHelper.parse_local_repo_package_version(repo_url)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_current_system_version() -> Optional[Version]:
|
||||
"""
|
||||
@@ -505,65 +479,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return None, None
|
||||
return user, repo
|
||||
|
||||
@cached(maxsize=1, ttl=1800)
|
||||
def get_statistic(self) -> Dict:
|
||||
"""
|
||||
获取插件安装统计
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return {}
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=10).get_res(self._install_statistic)
|
||||
if res is not None and res.status_code == 200:
|
||||
return res.json()
|
||||
return {}
|
||||
|
||||
def install_reg(self, pid: str, repo_url: Optional[str] = None) -> bool:
|
||||
"""
|
||||
安装插件统计
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
if not pid:
|
||||
return False
|
||||
install_reg_url = self._install_reg.format(pid=pid)
|
||||
res = RequestUtils(
|
||||
proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5
|
||||
).post(install_reg_url, json={
|
||||
"plugin_id": pid,
|
||||
"repo_url": self.sanitize_repo_url_for_statistic(repo_url)
|
||||
})
|
||||
if res is not None and res.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
|
||||
def install_report(self, items: Optional[List[Tuple[str, Optional[str]]]] = None) -> bool:
|
||||
"""
|
||||
上报存量插件安装统计(批量)。支持上送 repo_url。
|
||||
:param items: 可选,形如 [(plugin_id, repo_url), ...];不传则回落到历史配置,仅上送 plugin_id。
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
payload_plugins = []
|
||||
if items:
|
||||
for pid, repo_url in items:
|
||||
if pid:
|
||||
payload_plugins.append({
|
||||
"plugin_id": pid,
|
||||
"repo_url": self.sanitize_repo_url_for_statistic(repo_url)
|
||||
})
|
||||
else:
|
||||
plugins = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins)
|
||||
if not plugins:
|
||||
return False
|
||||
payload_plugins = [{"plugin_id": plugin, "repo_url": None} for plugin in plugins]
|
||||
res = RequestUtils(proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5).post(self._install_report,
|
||||
json={"plugins": payload_plugins})
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
|
||||
def install(self, pid: str, repo_url: str, package_version: Optional[str] = None, force_install: bool = False) \
|
||||
-> Tuple[bool, str]:
|
||||
"""
|
||||
@@ -1595,7 +1510,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
||||
return False, dep_msg
|
||||
|
||||
self.install_reg(pid, repo_url)
|
||||
self.refresh_persistent_plugin_backup(pid)
|
||||
return True, ""
|
||||
|
||||
@@ -1961,64 +1875,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return None
|
||||
return self.__parse_plugin_index_response(res.text)
|
||||
|
||||
async def async_get_statistic(self) -> Dict:
|
||||
"""
|
||||
异步获取插件安装统计
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return {}
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=10).get_res(self._install_statistic)
|
||||
if res is not None and res.status_code == 200:
|
||||
return res.json()
|
||||
return {}
|
||||
|
||||
async def async_install_reg(self, pid: str, repo_url: Optional[str] = None) -> bool:
|
||||
"""
|
||||
异步安装插件统计
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
if not pid:
|
||||
return False
|
||||
install_reg_url = self._install_reg.format(pid=pid)
|
||||
res = await AsyncRequestUtils(
|
||||
proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5
|
||||
).post(install_reg_url, json={
|
||||
"plugin_id": pid,
|
||||
"repo_url": self.sanitize_repo_url_for_statistic(repo_url)
|
||||
})
|
||||
if res is not None and res.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_install_report(self, items: Optional[List[Tuple[str, Optional[str]]]] = None) -> bool:
|
||||
"""
|
||||
异步上报存量插件安装统计(批量)。支持上送 repo_url。
|
||||
:param items: 可选,形如 [(plugin_id, repo_url), ...];不传则回落到历史配置,仅上送 plugin_id。
|
||||
"""
|
||||
if not settings.PLUGIN_STATISTIC_SHARE:
|
||||
return False
|
||||
payload_plugins = []
|
||||
if items:
|
||||
for pid, repo_url in items:
|
||||
if pid:
|
||||
payload_plugins.append({
|
||||
"plugin_id": pid,
|
||||
"repo_url": self.sanitize_repo_url_for_statistic(repo_url)
|
||||
})
|
||||
else:
|
||||
plugins = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins)
|
||||
if not plugins:
|
||||
return False
|
||||
payload_plugins = [{"plugin_id": plugin, "repo_url": None} for plugin in plugins]
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5).post(self._install_report,
|
||||
json={"plugins": payload_plugins})
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
|
||||
async def __async_get_file_list(self, pid: str, user_repo: str, package_version: Optional[str] = None) -> \
|
||||
Tuple[Optional[list], Optional[str]]:
|
||||
"""
|
||||
@@ -2503,7 +2359,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
||||
return False, dep_msg
|
||||
|
||||
await self.async_install_reg(pid, repo_url)
|
||||
await asyncio.to_thread(self.refresh_persistent_plugin_backup, pid)
|
||||
return True, ""
|
||||
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType, media_type_to_agent
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from app.utils.singleton import WeakSingleton
|
||||
|
||||
|
||||
class MediaRecognizeShareHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
共享媒体识别帮助类
|
||||
"""
|
||||
|
||||
_default_path = "/recognize/share"
|
||||
|
||||
@classmethod
|
||||
def _normalize_media_type(cls, media_type: Optional[object]) -> Optional[str]:
|
||||
"""
|
||||
统一媒体类型,兼容枚举、中文值和 agent 风格字符串
|
||||
"""
|
||||
normalized = media_type_to_agent(media_type)
|
||||
if normalized in {"movie", "tv"}:
|
||||
return normalized
|
||||
if isinstance(media_type, str):
|
||||
if media_type == MediaType.MOVIE.value:
|
||||
return "movie"
|
||||
if media_type == MediaType.TV.value:
|
||||
return "tv"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_keyword(meta: Optional[MetaBase]) -> Optional[str]:
|
||||
"""
|
||||
提取识别关键字
|
||||
"""
|
||||
if not meta:
|
||||
return None
|
||||
keyword = meta.original_name or meta.name
|
||||
if keyword:
|
||||
keyword = str(keyword).strip()
|
||||
return keyword or None
|
||||
|
||||
@classmethod
|
||||
def _extract_media_type(
|
||||
cls,
|
||||
meta: Optional[MetaBase] = None,
|
||||
mtype: Optional[MediaType] = None,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
提取媒体类型
|
||||
"""
|
||||
media_type = cls._normalize_media_type(mtype)
|
||||
if media_type:
|
||||
return media_type
|
||||
if mediainfo and mediainfo.type in {MediaType.MOVIE, MediaType.TV}:
|
||||
return mediainfo.type.to_agent()
|
||||
if meta and meta.type in {MediaType.MOVIE, MediaType.TV}:
|
||||
return meta.type.to_agent()
|
||||
if meta and (meta.begin_season is not None or meta.begin_episode is not None):
|
||||
return "tv"
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _extract_season(
|
||||
cls,
|
||||
media_type: Optional[str],
|
||||
meta: Optional[MetaBase] = None,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
提取季信息,仅电视剧使用
|
||||
"""
|
||||
if media_type != "tv":
|
||||
return None
|
||||
season = meta.begin_season if meta else None
|
||||
if season is None and mediainfo:
|
||||
season = mediainfo.season
|
||||
try:
|
||||
return int(season) if season is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_year(
|
||||
meta: Optional[MetaBase] = None,
|
||||
mediainfo: Optional[MediaInfo] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
提取年份
|
||||
"""
|
||||
year = (meta.year if meta else None) or (mediainfo.year if mediainfo else None)
|
||||
if year is None:
|
||||
return None
|
||||
year_text = str(year).strip()
|
||||
return year_text or None
|
||||
|
||||
@classmethod
|
||||
def _build_api_url(cls) -> Optional[str]:
|
||||
"""
|
||||
获取共享识别API地址
|
||||
"""
|
||||
custom_api = (settings.MEDIA_RECOGNIZE_SHARE_API or "").strip()
|
||||
if custom_api:
|
||||
return custom_api.rstrip("/")
|
||||
server_host = (settings.MP_SERVER_HOST or "").strip().rstrip("/")
|
||||
if not server_host:
|
||||
return None
|
||||
return f"{server_host}{cls._default_path}"
|
||||
|
||||
@classmethod
|
||||
def _build_query_params(
|
||||
cls,
|
||||
meta: Optional[MetaBase],
|
||||
mtype: Optional[MediaType] = None,
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
组装共享识别查询参数
|
||||
"""
|
||||
keyword = cls._extract_keyword(keyword_meta or meta)
|
||||
if not keyword:
|
||||
return None
|
||||
|
||||
media_type = cls._extract_media_type(meta=meta, mtype=mtype)
|
||||
params = {
|
||||
"keyword": keyword,
|
||||
}
|
||||
if media_type:
|
||||
params["type"] = media_type
|
||||
if year := cls._extract_year(meta=meta):
|
||||
params["year"] = year
|
||||
if season := cls._extract_season(media_type=media_type, meta=meta):
|
||||
params["season"] = season
|
||||
return params
|
||||
|
||||
@classmethod
|
||||
def _build_report_payload(
|
||||
cls,
|
||||
meta: Optional[MetaBase],
|
||||
mediainfo: Optional[MediaInfo],
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
组装共享识别上报载荷
|
||||
"""
|
||||
if not meta or not mediainfo:
|
||||
return None
|
||||
|
||||
keyword = cls._extract_keyword(keyword_meta or meta)
|
||||
media_type = cls._extract_media_type(meta=meta, mediainfo=mediainfo)
|
||||
if not keyword or not media_type:
|
||||
return None
|
||||
if not any([mediainfo.tmdb_id, mediainfo.douban_id, mediainfo.bangumi_id]):
|
||||
return None
|
||||
|
||||
return {
|
||||
"keyword": keyword,
|
||||
"type": media_type,
|
||||
"title": mediainfo.title or keyword,
|
||||
"year": cls._extract_year(meta=meta, mediainfo=mediainfo),
|
||||
"season": cls._extract_season(
|
||||
media_type=media_type,
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
),
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"doubanid": mediainfo.douban_id,
|
||||
"bangumiid": mediainfo.bangumi_id,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_response_item(data: Optional[dict]) -> Optional[dict]:
|
||||
"""
|
||||
解析服务端返回的共享识别数据
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
item = (data.get("data") or {}).get("item")
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def _response_message(response) -> str:
|
||||
"""
|
||||
获取响应消息,兼容非JSON响应
|
||||
"""
|
||||
try:
|
||||
payload = response.json()
|
||||
return str(payload.get("message") or "")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _is_enabled() -> bool:
|
||||
"""
|
||||
是否启用共享识别
|
||||
"""
|
||||
return bool(settings.MEDIA_RECOGNIZE_SHARE)
|
||||
|
||||
def query(
|
||||
self,
|
||||
meta: Optional[MetaBase],
|
||||
mtype: Optional[MediaType] = None,
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
查询共享识别结果
|
||||
"""
|
||||
if not self._is_enabled():
|
||||
return None
|
||||
|
||||
api_url = self._build_api_url()
|
||||
params = self._build_query_params(
|
||||
meta=meta,
|
||||
mtype=mtype,
|
||||
keyword_meta=keyword_meta,
|
||||
)
|
||||
if not api_url or not params:
|
||||
return None
|
||||
|
||||
response = RequestUtils(proxies=settings.PROXY or {}, timeout=5).get_res(
|
||||
api_url,
|
||||
params=params,
|
||||
)
|
||||
if not response or response.status_code != 200:
|
||||
if response is not None:
|
||||
logger.warn(
|
||||
f"查询共享媒体识别失败:status={response.status_code} "
|
||||
f"message={self._response_message(response)}"
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = response.json()
|
||||
except (json.JSONDecodeError, ValueError) as err:
|
||||
logger.warn(f"解析共享媒体识别响应失败:{err}")
|
||||
return None
|
||||
|
||||
if payload.get("code") != 0:
|
||||
return None
|
||||
|
||||
item = self._parse_response_item(payload)
|
||||
if item:
|
||||
logger.info(f"共享媒体识别命中:{params.get('keyword')} - {item}")
|
||||
return item
|
||||
|
||||
async def async_query(
|
||||
self,
|
||||
meta: Optional[MetaBase],
|
||||
mtype: Optional[MediaType] = None,
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
异步查询共享识别结果
|
||||
"""
|
||||
if not self._is_enabled():
|
||||
return None
|
||||
|
||||
api_url = self._build_api_url()
|
||||
params = self._build_query_params(
|
||||
meta=meta,
|
||||
mtype=mtype,
|
||||
keyword_meta=keyword_meta,
|
||||
)
|
||||
if not api_url or not params:
|
||||
return None
|
||||
|
||||
response = await AsyncRequestUtils(
|
||||
proxies=settings.PROXY or {},
|
||||
timeout=5,
|
||||
).get_res(api_url, params=params)
|
||||
if not response or response.status_code != 200:
|
||||
if response is not None:
|
||||
logger.warn(
|
||||
f"异步查询共享媒体识别失败:status={response.status_code} "
|
||||
f"message={self._response_message(response)}"
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = response.json()
|
||||
except (json.JSONDecodeError, ValueError) as err:
|
||||
logger.warn(f"解析共享媒体识别响应失败:{err}")
|
||||
return None
|
||||
|
||||
if payload.get("code") != 0:
|
||||
return None
|
||||
|
||||
item = self._parse_response_item(payload)
|
||||
if item:
|
||||
logger.info(f"共享媒体识别命中:{params.get('keyword')} - {item}")
|
||||
return item
|
||||
|
||||
def report(
|
||||
self,
|
||||
meta: Optional[MetaBase],
|
||||
mediainfo: Optional[MediaInfo],
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
上报共享识别结果
|
||||
"""
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
api_url = self._build_api_url()
|
||||
payload = self._build_report_payload(
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
keyword_meta=keyword_meta,
|
||||
)
|
||||
if not api_url or not payload:
|
||||
return False
|
||||
|
||||
response = RequestUtils(
|
||||
proxies=settings.PROXY or {},
|
||||
timeout=5,
|
||||
content_type="application/json",
|
||||
).post_res(api_url, json=payload)
|
||||
if not response or response.status_code != 200:
|
||||
if response is not None:
|
||||
logger.warn(
|
||||
f"上报共享媒体识别失败:status={response.status_code} "
|
||||
f"message={self._response_message(response)}"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
result = response.json()
|
||||
except (json.JSONDecodeError, ValueError) as err:
|
||||
logger.warn(f"解析共享媒体识别上报响应失败:{err}")
|
||||
return False
|
||||
|
||||
return result.get("code") == 0
|
||||
|
||||
async def async_report(
|
||||
self,
|
||||
meta: Optional[MetaBase],
|
||||
mediainfo: Optional[MediaInfo],
|
||||
keyword_meta: Optional[MetaBase] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
异步上报共享识别结果
|
||||
"""
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
api_url = self._build_api_url()
|
||||
payload = self._build_report_payload(
|
||||
meta=meta,
|
||||
mediainfo=mediainfo,
|
||||
keyword_meta=keyword_meta,
|
||||
)
|
||||
if not api_url or not payload:
|
||||
return False
|
||||
|
||||
response = await AsyncRequestUtils(
|
||||
proxies=settings.PROXY or {},
|
||||
timeout=5,
|
||||
content_type="application/json",
|
||||
).post_res(api_url, json=payload)
|
||||
if not response or response.status_code != 200:
|
||||
if response is not None:
|
||||
logger.warn(
|
||||
f"异步上报共享媒体识别失败:status={response.status_code} "
|
||||
f"message={self._response_message(response)}"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
result = response.json()
|
||||
except (json.JSONDecodeError, ValueError) as err:
|
||||
logger.warn(f"解析共享媒体识别上报响应失败:{err}")
|
||||
return False
|
||||
|
||||
return result.get("code") == 0
|
||||
|
||||
@classmethod
|
||||
def to_recognize_params(cls, item: Optional[dict]) -> Optional[dict]:
|
||||
"""
|
||||
将服务端返回的共享识别结果转成本地识别参数
|
||||
"""
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
|
||||
media_type = cls._normalize_media_type(item.get("type"))
|
||||
mtype = MediaType.from_agent(media_type) if media_type else None
|
||||
tmdbid = item.get("tmdbid")
|
||||
doubanid = item.get("doubanid")
|
||||
bangumiid = item.get("bangumiid")
|
||||
if not any([tmdbid, doubanid, bangumiid]):
|
||||
return None
|
||||
|
||||
return {
|
||||
"mtype": mtype,
|
||||
"tmdbid": tmdbid,
|
||||
"doubanid": doubanid,
|
||||
"bangumiid": bangumiid,
|
||||
"season": item.get("season"),
|
||||
}
|
||||
1654
app/helper/server.py
Normal file
1654
app/helper/server.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,506 +0,0 @@
|
||||
from threading import Thread
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from app.core.cache import cached
|
||||
from app.core.config import settings
|
||||
from app.db.subscribe_oper import SubscribeOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from app.utils.singleton import WeakSingleton
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class SubscribeHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
订阅数据统计/订阅分享等
|
||||
"""
|
||||
|
||||
_sub_reg = f"{settings.MP_SERVER_HOST}/subscribe/add"
|
||||
|
||||
_sub_done = f"{settings.MP_SERVER_HOST}/subscribe/done"
|
||||
|
||||
_sub_report = f"{settings.MP_SERVER_HOST}/subscribe/report"
|
||||
|
||||
_sub_statistic = f"{settings.MP_SERVER_HOST}/subscribe/statistic"
|
||||
|
||||
_sub_share = f"{settings.MP_SERVER_HOST}/subscribe/share"
|
||||
|
||||
_sub_shares = f"{settings.MP_SERVER_HOST}/subscribe/shares"
|
||||
|
||||
_sub_share_statistic = f"{settings.MP_SERVER_HOST}/subscribe/share/statistics"
|
||||
|
||||
_sub_fork = f"{settings.MP_SERVER_HOST}/subscribe/fork/%s"
|
||||
|
||||
_shares_cache_region = "subscribe_share"
|
||||
|
||||
_github_user = None
|
||||
|
||||
_share_user_id = None
|
||||
|
||||
_admin_users = [
|
||||
"jxxghp",
|
||||
"thsrite",
|
||||
"InfinityPacer",
|
||||
"DDSRem",
|
||||
"Aqr-K",
|
||||
"Putarku",
|
||||
"4Nest",
|
||||
"xyswordzoro",
|
||||
"wikrin"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
systemconfig = SystemConfigOper()
|
||||
if settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
if not systemconfig.get(SystemConfigKey.SubscribeReport):
|
||||
if self.sub_report():
|
||||
systemconfig.set(SystemConfigKey.SubscribeReport, "1")
|
||||
self.get_user_uuid()
|
||||
self.get_github_user()
|
||||
|
||||
@staticmethod
|
||||
def _check_subscribe_share_enabled() -> Tuple[bool, str]:
|
||||
"""
|
||||
检查订阅分享功能是否开启
|
||||
"""
|
||||
if not settings.SUBSCRIBE_STATISTIC_SHARE:
|
||||
return False, "当前没有开启订阅数据共享功能"
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def _validate_subscribe(subscribe) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证订阅是否存在
|
||||
"""
|
||||
if not subscribe:
|
||||
return False, "订阅不存在"
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def _prepare_subscribe_data(subscribe) -> dict:
|
||||
"""
|
||||
准备订阅分享数据
|
||||
"""
|
||||
subscribe_dict = subscribe.to_dict()
|
||||
subscribe_dict.pop("id", None)
|
||||
return subscribe_dict
|
||||
|
||||
def _build_share_payload(self, share_title: str, share_comment: str,
|
||||
share_user: str, subscribe_dict: dict) -> dict:
|
||||
"""
|
||||
构建分享请求载荷
|
||||
"""
|
||||
return {
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": self._share_user_id,
|
||||
**subscribe_dict
|
||||
}
|
||||
|
||||
def _handle_response(self, res, clear_cache: bool = True) -> Tuple[bool, str]:
|
||||
"""
|
||||
处理HTTP响应
|
||||
"""
|
||||
if res is None:
|
||||
return False, "连接MoviePilot服务器失败"
|
||||
|
||||
# 检查响应状态
|
||||
if res.status_code == 200:
|
||||
# 清除缓存
|
||||
if clear_cache:
|
||||
self.get_shares.cache_clear()
|
||||
self.get_statistic.cache_clear()
|
||||
self.get_share_statistics.cache_clear()
|
||||
self.async_get_shares.cache_clear()
|
||||
self.async_get_statistic.cache_clear()
|
||||
self.async_get_share_statistics.cache_clear()
|
||||
return True, ""
|
||||
else:
|
||||
return False, res.json().get("message")
|
||||
|
||||
@staticmethod
|
||||
def _handle_list_response(res) -> List[dict]:
|
||||
"""
|
||||
处理返回List的HTTP响应
|
||||
"""
|
||||
if res is not None and res.status_code == 200:
|
||||
return res.json()
|
||||
return []
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=5, ttl=1800, skip_empty=True)
|
||||
def get_statistic(self, stype: str, page: Optional[int] = 1, count: Optional[int] = 30,
|
||||
genre_id: Optional[int] = None, min_rating: Optional[float] = None,
|
||||
max_rating: Optional[float] = None, sort_type: Optional[str] = None) -> List[dict]:
|
||||
"""
|
||||
获取订阅统计数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
params = {
|
||||
"stype": stype,
|
||||
"page": page,
|
||||
"count": count
|
||||
}
|
||||
|
||||
# 添加可选参数
|
||||
if genre_id is not None:
|
||||
params["genre_id"] = genre_id
|
||||
if min_rating is not None:
|
||||
params["min_rating"] = min_rating
|
||||
if max_rating is not None:
|
||||
params["max_rating"] = max_rating
|
||||
if sort_type is not None:
|
||||
params["sort_type"] = sort_type
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_statistic, params=params)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=5, ttl=1800, skip_empty=True)
|
||||
async def async_get_statistic(self, stype: str, page: Optional[int] = 1, count: Optional[int] = 30,
|
||||
genre_id: Optional[int] = None, min_rating: Optional[float] = None,
|
||||
max_rating: Optional[float] = None, sort_type: Optional[str] = None) -> List[dict]:
|
||||
"""
|
||||
异步获取订阅统计数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
params = {
|
||||
"stype": stype,
|
||||
"page": page,
|
||||
"count": count
|
||||
}
|
||||
|
||||
# 添加可选参数
|
||||
if genre_id is not None:
|
||||
params["genre_id"] = genre_id
|
||||
if min_rating is not None:
|
||||
params["min_rating"] = min_rating
|
||||
if max_rating is not None:
|
||||
params["max_rating"] = max_rating
|
||||
if sort_type is not None:
|
||||
params["sort_type"] = sort_type
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_statistic, params=params)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
def sub_reg(self, sub: dict) -> bool:
|
||||
"""
|
||||
新增订阅统计
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).post_res(self._sub_reg, json=sub)
|
||||
if res is not None and res.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_sub_reg(self, sub: dict) -> bool:
|
||||
"""
|
||||
异步新增订阅统计
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).post_res(self._sub_reg, json=sub)
|
||||
if res is not None and res.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sub_done(self, sub: dict) -> bool:
|
||||
"""
|
||||
完成订阅统计
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).post_res(self._sub_done, json=sub)
|
||||
if res and res.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sub_reg_async(self, sub: dict) -> bool:
|
||||
"""
|
||||
异步新增订阅统计
|
||||
"""
|
||||
# 开新线程处理
|
||||
Thread(target=self.sub_reg, args=(sub,)).start()
|
||||
return True
|
||||
|
||||
def sub_done_async(self, sub: dict) -> bool:
|
||||
"""
|
||||
异步完成订阅统计
|
||||
"""
|
||||
# 开新线程处理
|
||||
Thread(target=self.sub_done, args=(sub,)).start()
|
||||
return True
|
||||
|
||||
def sub_report(self) -> bool:
|
||||
"""
|
||||
上报存量订阅统计
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False
|
||||
subscribes = SubscribeOper().list()
|
||||
if not subscribes:
|
||||
return True
|
||||
res = RequestUtils(proxies=settings.PROXY, content_type="application/json",
|
||||
timeout=10).post(self._sub_report,
|
||||
json={
|
||||
"subscribes": [
|
||||
sub.to_dict() for sub in subscribes
|
||||
]
|
||||
})
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
|
||||
def sub_share(self, subscribe_id: int,
|
||||
share_title: str, share_comment: str, share_user: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
分享订阅
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
# 获取订阅信息
|
||||
subscribe = SubscribeOper().get(subscribe_id)
|
||||
|
||||
# 验证订阅
|
||||
valid, message = self._validate_subscribe(subscribe)
|
||||
if not valid:
|
||||
return False, message
|
||||
|
||||
# 准备数据
|
||||
subscribe_dict = self._prepare_subscribe_data(subscribe)
|
||||
payload = self._build_share_payload(share_title, share_comment, share_user, subscribe_dict)
|
||||
|
||||
# 发送分享请求
|
||||
res = RequestUtils(proxies=settings.PROXY, content_type="application/json",
|
||||
timeout=10).post(self._sub_share, json=payload)
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
async def async_sub_share(self, subscribe_id: int,
|
||||
share_title: str, share_comment: str, share_user: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步分享订阅
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
# 获取订阅信息
|
||||
subscribe = await SubscribeOper().async_get(subscribe_id)
|
||||
|
||||
# 验证订阅
|
||||
valid, message = self._validate_subscribe(subscribe)
|
||||
if not valid:
|
||||
return False, message
|
||||
|
||||
# 准备数据
|
||||
subscribe_dict = self._prepare_subscribe_data(subscribe)
|
||||
payload = self._build_share_payload(share_title, share_comment, share_user, subscribe_dict)
|
||||
|
||||
# 发送分享请求
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, content_type="application/json",
|
||||
timeout=10).post(self._sub_share, json=payload)
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
def share_delete(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
删除分享
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY,
|
||||
timeout=5).delete_res(f"{self._sub_share}/{share_id}",
|
||||
params={"share_uid": self._share_user_id})
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
async def async_share_delete(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步删除分享
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY,
|
||||
timeout=5).delete_res(f"{self._sub_share}/{share_id}",
|
||||
params={"share_uid": self._share_user_id})
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
def sub_fork(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
复用分享的订阅
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).get_res(self._sub_fork % share_id)
|
||||
|
||||
return self._handle_response(res, clear_cache=False)
|
||||
|
||||
async def async_sub_fork(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步复用分享的订阅
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).get_res(self._sub_fork % share_id)
|
||||
|
||||
return self._handle_response(res, clear_cache=False)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, ttl=1800, skip_empty=True)
|
||||
def get_shares(self, name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30,
|
||||
genre_id: Optional[int] = None, min_rating: Optional[float] = None,
|
||||
max_rating: Optional[float] = None, sort_type: Optional[str] = None) -> List[dict]:
|
||||
"""
|
||||
获取订阅分享数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
params = {
|
||||
"name": name,
|
||||
"page": page,
|
||||
"count": count
|
||||
}
|
||||
|
||||
# 添加可选参数
|
||||
if genre_id is not None:
|
||||
params["genre_id"] = genre_id
|
||||
if min_rating is not None:
|
||||
params["min_rating"] = min_rating
|
||||
if max_rating is not None:
|
||||
params["max_rating"] = max_rating
|
||||
if sort_type is not None:
|
||||
params["sort_type"] = sort_type
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_shares, params=params)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, ttl=1800, skip_empty=True)
|
||||
async def async_get_shares(self, name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30,
|
||||
genre_id: Optional[int] = None, min_rating: Optional[float] = None,
|
||||
max_rating: Optional[float] = None, sort_type: Optional[str] = None) -> List[dict]:
|
||||
"""
|
||||
异步获取订阅分享数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
params = {
|
||||
"name": name,
|
||||
"page": page,
|
||||
"count": count
|
||||
}
|
||||
|
||||
# 添加可选参数
|
||||
if genre_id is not None:
|
||||
params["genre_id"] = genre_id
|
||||
if min_rating is not None:
|
||||
params["min_rating"] = min_rating
|
||||
if max_rating is not None:
|
||||
params["max_rating"] = max_rating
|
||||
if sort_type is not None:
|
||||
params["sort_type"] = sort_type
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_shares, params=params)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, ttl=1800, skip_empty=True)
|
||||
def get_share_statistics(self) -> List[dict]:
|
||||
"""
|
||||
获取订阅分享统计数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_share_statistic)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, ttl=1800, skip_empty=True)
|
||||
async def async_get_share_statistics(self) -> List[dict]:
|
||||
"""
|
||||
异步获取订阅分享统计数据
|
||||
"""
|
||||
enabled, _ = self._check_subscribe_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=15).get_res(self._sub_share_statistic)
|
||||
|
||||
return self._handle_list_response(res)
|
||||
|
||||
def get_user_uuid(self) -> str:
|
||||
"""
|
||||
获取用户uuid
|
||||
"""
|
||||
if not self._share_user_id:
|
||||
self._share_user_id = SystemUtils.generate_user_unique_id()
|
||||
logger.info(f"当前用户UUID: {self._share_user_id}")
|
||||
return self._share_user_id
|
||||
|
||||
def get_github_user(self) -> str:
|
||||
"""
|
||||
获取github用户
|
||||
"""
|
||||
if self._github_user is None and settings.GITHUB_HEADERS:
|
||||
res = RequestUtils(headers=settings.GITHUB_HEADERS,
|
||||
proxies=settings.PROXY,
|
||||
timeout=15).get_res(f"https://api.github.com/user")
|
||||
if res:
|
||||
self._github_user = res.json().get("login")
|
||||
logger.info(f"当前Github用户: {self._github_user}")
|
||||
return self._github_user
|
||||
|
||||
def is_admin_user(self) -> bool:
|
||||
"""
|
||||
判断是否是管理员
|
||||
"""
|
||||
if not self._github_user:
|
||||
return False
|
||||
if self._github_user in self._admin_users:
|
||||
return True
|
||||
return False
|
||||
@@ -1,119 +0,0 @@
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.utils.http import AsyncRequestUtils, RequestUtils
|
||||
from app.utils.singleton import WeakSingleton
|
||||
from app.utils.system import SystemUtils
|
||||
from version import APP_VERSION, FRONTEND_VERSION
|
||||
|
||||
|
||||
class UsageHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
安装版本统计上报
|
||||
"""
|
||||
|
||||
_usage_report = f"{settings.MP_SERVER_HOST}/usage/report"
|
||||
_usage_statistic = f"{settings.MP_SERVER_HOST}/usage/statistic"
|
||||
|
||||
@staticmethod
|
||||
def get_frontend_version() -> str:
|
||||
"""
|
||||
获取当前前端版本。
|
||||
"""
|
||||
if SystemUtils.is_frozen() and SystemUtils.is_windows():
|
||||
version_file = settings.CONFIG_PATH.parent / "nginx" / "html" / "version.txt"
|
||||
else:
|
||||
version_file = Path(settings.FRONTEND_PATH) / "version.txt"
|
||||
if version_file.exists():
|
||||
try:
|
||||
with open(version_file, "r") as file:
|
||||
version = str(file.read()).strip()
|
||||
return version or FRONTEND_VERSION
|
||||
except Exception as err:
|
||||
logger.debug(f"加载版本文件 {version_file} 出错:{str(err)}")
|
||||
return FRONTEND_VERSION
|
||||
|
||||
@staticmethod
|
||||
def build_payload() -> Dict[str, Any]:
|
||||
"""
|
||||
构建安装版本统计上报载荷。
|
||||
"""
|
||||
return {
|
||||
"user_uid": SystemUtils.generate_user_unique_id(),
|
||||
"backend_version": APP_VERSION,
|
||||
"frontend_version": UsageHelper.get_frontend_version(),
|
||||
"version_flag": settings.VERSION_FLAG,
|
||||
"platform": f"{platform.system()} {platform.release()}".strip(),
|
||||
"arch": SystemUtils.cpu_arch(),
|
||||
}
|
||||
|
||||
def report(self) -> bool:
|
||||
"""
|
||||
上报当前安装实例的版本统计。
|
||||
"""
|
||||
if not settings.USAGE_STATISTIC_SHARE:
|
||||
return False
|
||||
payload = self.build_payload()
|
||||
if not payload.get("user_uid"):
|
||||
return False
|
||||
try:
|
||||
res = RequestUtils(
|
||||
proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5,
|
||||
).post(self._usage_report, json=payload)
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
except Exception as err:
|
||||
logger.debug(f"上报安装版本统计失败:{str(err)}")
|
||||
return False
|
||||
|
||||
async def async_report(self) -> bool:
|
||||
"""
|
||||
异步上报当前安装实例的版本统计。
|
||||
"""
|
||||
if not settings.USAGE_STATISTIC_SHARE:
|
||||
return False
|
||||
payload = self.build_payload()
|
||||
if not payload.get("user_uid"):
|
||||
return False
|
||||
try:
|
||||
res = await AsyncRequestUtils(
|
||||
proxies=settings.PROXY,
|
||||
content_type="application/json",
|
||||
timeout=5,
|
||||
).post(self._usage_report, json=payload)
|
||||
return bool(res is not None and res.status_code == 200)
|
||||
except Exception as err:
|
||||
logger.debug(f"异步上报安装版本统计失败:{str(err)}")
|
||||
return False
|
||||
|
||||
def get_statistic(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取安装版本统计报表。
|
||||
"""
|
||||
if not settings.USAGE_STATISTIC_SHARE:
|
||||
return {}
|
||||
try:
|
||||
res = RequestUtils(proxies=settings.PROXY, timeout=10).get_res(self._usage_statistic)
|
||||
if res is not None and res.status_code == 200:
|
||||
return res.json()
|
||||
except Exception as err:
|
||||
logger.debug(f"获取安装版本统计报表失败:{str(err)}")
|
||||
return {}
|
||||
|
||||
async def async_get_statistic(self) -> Dict[str, Any]:
|
||||
"""
|
||||
异步获取安装版本统计报表。
|
||||
"""
|
||||
if not settings.USAGE_STATISTIC_SHARE:
|
||||
return {}
|
||||
try:
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY, timeout=10).get_res(self._usage_statistic)
|
||||
if res is not None and res.status_code == 200:
|
||||
return res.json()
|
||||
except Exception as err:
|
||||
logger.debug(f"异步获取安装版本统计报表失败:{str(err)}")
|
||||
return {}
|
||||
@@ -1,276 +0,0 @@
|
||||
import json
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from app.core.cache import cached
|
||||
from app.core.config import settings
|
||||
from app.db.models import Workflow
|
||||
from app.db.workflow_oper import WorkflowOper
|
||||
from app.log import logger
|
||||
from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from app.utils.singleton import WeakSingleton
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class WorkflowHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
工作流分享等
|
||||
"""
|
||||
|
||||
_workflow_share = f"{settings.MP_SERVER_HOST}/workflow/share"
|
||||
|
||||
_workflow_shares = f"{settings.MP_SERVER_HOST}/workflow/shares"
|
||||
|
||||
_workflow_fork = f"{settings.MP_SERVER_HOST}/workflow/fork/%s"
|
||||
|
||||
_shares_cache_region = "workflow_share"
|
||||
|
||||
_share_user_id = None
|
||||
|
||||
def __init__(self):
|
||||
self.get_user_uuid()
|
||||
|
||||
@staticmethod
|
||||
def _check_workflow_share_enabled() -> Tuple[bool, str]:
|
||||
"""
|
||||
检查工作流分享功能是否开启
|
||||
"""
|
||||
if not settings.WORKFLOW_STATISTIC_SHARE:
|
||||
return False, "当前没有开启工作流数据共享功能"
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def _validate_workflow(workflow: Workflow) -> Tuple[bool, str]:
|
||||
"""
|
||||
验证工作流是否可以分享
|
||||
"""
|
||||
if not workflow:
|
||||
return False, "工作流不存在"
|
||||
|
||||
if not workflow.actions or not workflow.flows:
|
||||
return False, "请分享有动作和流程的工作流"
|
||||
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def _prepare_workflow_data(workflow: 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
|
||||
|
||||
def _build_share_payload(self, share_title: str, share_comment: str,
|
||||
share_user: str, workflow_dict: dict) -> dict:
|
||||
"""
|
||||
构建分享请求载荷
|
||||
"""
|
||||
return {
|
||||
"share_title": share_title,
|
||||
"share_comment": share_comment,
|
||||
"share_user": share_user,
|
||||
"share_uid": self._share_user_id,
|
||||
**workflow_dict
|
||||
}
|
||||
|
||||
def _handle_response(self, res, clear_cache: bool = True) -> Tuple[bool, str]:
|
||||
"""
|
||||
处理HTTP响应
|
||||
"""
|
||||
if res is None:
|
||||
return False, "连接MoviePilot服务器失败"
|
||||
|
||||
# 检查响应状态
|
||||
success = True if res.status_code == 200 else False
|
||||
|
||||
if success:
|
||||
# 清除缓存
|
||||
if clear_cache:
|
||||
self.get_shares.cache_clear()
|
||||
self.async_get_shares.cache_clear()
|
||||
return True, ""
|
||||
else:
|
||||
try:
|
||||
error_msg = res.json().get("message", "未知错误")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.error(f"工作流响应JSON解析失败: {e}")
|
||||
error_msg = f"响应解析失败: {res.text[:100]}..."
|
||||
return False, error_msg
|
||||
|
||||
@staticmethod
|
||||
def _handle_list_response(res) -> List[dict]:
|
||||
"""
|
||||
处理返回List的HTTP响应
|
||||
"""
|
||||
if res and res.status_code == 200:
|
||||
try:
|
||||
return res.json()
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.error(f"工作流列表响应JSON解析失败: {e}")
|
||||
return []
|
||||
return []
|
||||
|
||||
def workflow_share(self, workflow_id: int,
|
||||
share_title: str, share_comment: str, share_user: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
分享工作流
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
# 获取工作流信息
|
||||
workflow = WorkflowOper().get(workflow_id)
|
||||
|
||||
# 验证工作流
|
||||
valid, message = self._validate_workflow(workflow)
|
||||
if not valid:
|
||||
return False, message
|
||||
|
||||
# 准备数据
|
||||
workflow_dict = self._prepare_workflow_data(workflow)
|
||||
payload = self._build_share_payload(share_title, share_comment, share_user, workflow_dict)
|
||||
|
||||
# 发送分享请求
|
||||
res = RequestUtils(proxies=settings.PROXY or {},
|
||||
content_type="application/json",
|
||||
timeout=10).post(self._workflow_share, json=payload)
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
async def async_workflow_share(self, workflow_id: int,
|
||||
share_title: str, share_comment: str, share_user: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步分享工作流
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
# 获取工作流信息
|
||||
workflow = await WorkflowOper().async_get(workflow_id)
|
||||
|
||||
# 验证工作流
|
||||
valid, message = self._validate_workflow(workflow)
|
||||
if not valid:
|
||||
return False, message
|
||||
|
||||
# 准备数据
|
||||
workflow_dict = self._prepare_workflow_data(workflow)
|
||||
payload = self._build_share_payload(share_title, share_comment, share_user, workflow_dict)
|
||||
|
||||
# 发送分享请求
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY or {},
|
||||
content_type="application/json",
|
||||
timeout=10).post(self._workflow_share, json=payload)
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
def share_delete(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
删除分享
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY or {},
|
||||
timeout=5).delete_res(f"{self._workflow_share}/{share_id}",
|
||||
params={"share_uid": self._share_user_id})
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
async def async_share_delete(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步删除分享
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY or {},
|
||||
timeout=5).delete_res(f"{self._workflow_share}/{share_id}",
|
||||
params={"share_uid": self._share_user_id})
|
||||
|
||||
return self._handle_response(res)
|
||||
|
||||
def workflow_fork(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
复用分享的工作流
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY or {}, timeout=5, headers={
|
||||
"Content-Type": "application/json"
|
||||
}).get_res(self._workflow_fork % share_id)
|
||||
|
||||
return self._handle_response(res, clear_cache=False)
|
||||
|
||||
async def async_workflow_fork(self, share_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
异步复用分享的工作流
|
||||
"""
|
||||
# 检查功能是否开启
|
||||
enabled, message = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return False, message
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY or {},
|
||||
timeout=5,
|
||||
headers={
|
||||
"Content-Type": "application/json"
|
||||
}).get_res(self._workflow_fork % share_id)
|
||||
|
||||
return self._handle_response(res, clear_cache=False)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, skip_empty=True)
|
||||
def get_shares(self, name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||
"""
|
||||
获取工作流分享数据
|
||||
"""
|
||||
enabled, _ = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
res = RequestUtils(proxies=settings.PROXY or {}, timeout=15).get_res(self._workflow_shares, params={
|
||||
"name": name,
|
||||
"page": page,
|
||||
"count": count
|
||||
})
|
||||
return self._handle_list_response(res)
|
||||
|
||||
@cached(region=_shares_cache_region, maxsize=1, skip_empty=True)
|
||||
async def async_get_shares(self, name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30) -> \
|
||||
List[dict]:
|
||||
"""
|
||||
异步获取工作流分享数据
|
||||
"""
|
||||
enabled, _ = self._check_workflow_share_enabled()
|
||||
if not enabled:
|
||||
return []
|
||||
|
||||
res = await AsyncRequestUtils(proxies=settings.PROXY or {}, timeout=15).get_res(self._workflow_shares, params={
|
||||
"name": name,
|
||||
"page": page,
|
||||
"count": count
|
||||
})
|
||||
return self._handle_list_response(res)
|
||||
|
||||
def get_user_uuid(self) -> str:
|
||||
"""
|
||||
获取用户uuid
|
||||
"""
|
||||
if not self._share_user_id:
|
||||
self._share_user_id = SystemUtils.generate_user_unique_id()
|
||||
logger.info(f"当前用户UUID: {self._share_user_id}")
|
||||
return self._share_user_id or ""
|
||||
@@ -36,7 +36,7 @@ from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.image import WallpaperHelper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.helper.sites import SitesHelper # noqa
|
||||
from app.helper.usage import UsageHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification, NotificationType, Workflow
|
||||
from app.schemas.types import EventType, SystemConfigKey
|
||||
@@ -405,7 +405,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
|
||||
},
|
||||
"usage_report": {
|
||||
"name": "安装版本统计上报",
|
||||
"func": UsageHelper().report,
|
||||
"func": MoviePilotServerHelper.report_usage,
|
||||
"running": False,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ from fastapi import FastAPI
|
||||
|
||||
from app.chain.system import SystemChain
|
||||
from app.core.config import global_vars
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.helper.system import SystemHelper
|
||||
from app.helper.usage import UsageHelper
|
||||
from app.startup.command_initializer import init_command, stop_command, restart_command
|
||||
from app.startup.modules_initializer import init_modules, stop_modules
|
||||
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
||||
@@ -31,7 +31,7 @@ async def init_extra():
|
||||
# 重启完成
|
||||
SystemChain().restart_finish()
|
||||
# 上报当前安装版本
|
||||
await UsageHelper().async_report()
|
||||
await MoviePilotServerHelper.async_report_usage()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.helper.display import DisplayHelper
|
||||
from app.helper.doh import DohHelper
|
||||
from app.helper.resource import ResourceHelper
|
||||
from app.helper.message import MessageHelper, stop_message
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.db import close_database
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.command import CommandChain
|
||||
@@ -152,8 +152,11 @@ def init_modules():
|
||||
ModuleManager()
|
||||
# 启动事件消费
|
||||
EventManager().start()
|
||||
# 初始化订阅分享
|
||||
SubscribeHelper()
|
||||
# 初始化共享服务端状态
|
||||
MoviePilotServerHelper.init_plugin_report()
|
||||
MoviePilotServerHelper.init_subscribe_report()
|
||||
MoviePilotServerHelper.get_user_uuid()
|
||||
MoviePilotServerHelper.get_github_user()
|
||||
# 初始化AI智能体
|
||||
init_agent()
|
||||
# 启动前端服务
|
||||
|
||||
Reference in New Issue
Block a user