mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +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.config import settings
|
||||||
from app.core.plugin import PluginManager
|
from app.core.plugin import PluginManager
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.plugin import PluginHelper
|
from app.helper.plugin import PluginHelper
|
||||||
from app.schemas.types import SystemConfigKey
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ async def install_plugin_runtime(
|
|||||||
refreshed_only = False
|
refreshed_only = False
|
||||||
if not force and plugin_id in plugin_manager.get_plugin_ids():
|
if not force and plugin_id in plugin_manager.get_plugin_ids():
|
||||||
refreshed_only = True
|
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 = "插件已存在,已刷新加载"
|
message = "插件已存在,已刷新加载"
|
||||||
else:
|
else:
|
||||||
if not repo_url:
|
if not repo_url:
|
||||||
@@ -242,6 +243,7 @@ async def install_plugin_runtime(
|
|||||||
)
|
)
|
||||||
if not state:
|
if not state:
|
||||||
return False, message, False
|
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:
|
if plugin_id not in install_plugins:
|
||||||
install_plugins.append(plugin_id)
|
install_plugins.append(plugin_id)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
|||||||
from app.agent.tools.base import MoviePilotTool
|
from app.agent.tools.base import MoviePilotTool
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
from app.db.subscribe_oper import SubscribeOper
|
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.log import logger
|
||||||
from app.schemas.types import EventType
|
from app.schemas.types import EventType
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class DeleteSubscribeTool(MoviePilotTool):
|
|||||||
|
|
||||||
await subscribe_oper.async_delete(subscribe_id)
|
await subscribe_oper.async_delete(subscribe_id)
|
||||||
# 分享订阅统计刷新本身已异步化,这里只需要在删除后触发即可。
|
# 分享订阅统计刷新本身已异步化,这里只需要在删除后触发即可。
|
||||||
SubscribeHelper().sub_done_async(
|
MoviePilotServerHelper.sub_done_async(
|
||||||
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from app.agent.tools.base import MoviePilotTool
|
from app.agent.tools.base import MoviePilotTool
|
||||||
from app.core.context import MediaInfo
|
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.log import logger
|
||||||
from app.schemas.types import MediaType, media_type_to_agent
|
from app.schemas.types import MediaType, media_type_to_agent
|
||||||
|
|
||||||
@@ -77,8 +77,7 @@ class QueryPopularSubscribesTool(MoviePilotTool):
|
|||||||
if not media_type_enum:
|
if not media_type_enum:
|
||||||
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
|
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv'"
|
||||||
|
|
||||||
subscribe_helper = SubscribeHelper()
|
subscribes = await MoviePilotServerHelper.async_get_subscribe_statistic(
|
||||||
subscribes = await subscribe_helper.async_get_statistic(
|
|
||||||
stype=media_type_enum.to_agent(),
|
stype=media_type_enum.to_agent(),
|
||||||
page=page,
|
page=page,
|
||||||
count=count,
|
count=count,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Optional, Type
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.agent.tools.base import MoviePilotTool
|
from app.agent.tools.base import MoviePilotTool
|
||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
|
||||||
MAX_PAGE_SIZE = 50
|
MAX_PAGE_SIZE = 50
|
||||||
@@ -68,8 +68,7 @@ class QuerySubscribeSharesTool(MoviePilotTool):
|
|||||||
# 订阅分享是外部列表型结果,限制单页大小能降低工具上下文占用。
|
# 订阅分享是外部列表型结果,限制单页大小能降低工具上下文占用。
|
||||||
count = min(count, MAX_PAGE_SIZE)
|
count = min(count, MAX_PAGE_SIZE)
|
||||||
|
|
||||||
subscribe_helper = SubscribeHelper()
|
shares = await MoviePilotServerHelper.async_get_subscribe_shares(
|
||||||
shares = await subscribe_helper.async_get_shares(
|
|
||||||
name=name,
|
name=name,
|
||||||
page=page,
|
page=page,
|
||||||
count=count,
|
count=count,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.db.user_oper import (
|
|||||||
get_current_active_superuser_async,
|
get_current_active_superuser_async,
|
||||||
)
|
)
|
||||||
from app.factory import app
|
from app.factory import app
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.plugin import PluginHelper
|
from app.helper.plugin import PluginHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.scheduler import Scheduler
|
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(
|
@router.get(
|
||||||
@@ -257,7 +258,7 @@ async def install(
|
|||||||
)
|
)
|
||||||
if compatible_message:
|
if compatible_message:
|
||||||
return schemas.Response(success=False, message=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:
|
else:
|
||||||
# 插件不存在或需要强制安装,下载安装并注册插件
|
# 插件不存在或需要强制安装,下载安装并注册插件
|
||||||
if repo_url:
|
if repo_url:
|
||||||
@@ -267,6 +268,7 @@ async def install(
|
|||||||
# 安装失败则直接响应
|
# 安装失败则直接响应
|
||||||
if not state:
|
if not state:
|
||||||
return schemas.Response(success=False, message=msg)
|
return schemas.Response(success=False, message=msg)
|
||||||
|
await MoviePilotServerHelper.async_install_plugin_reg(plugin_id=plugin_id, repo_url=repo_url)
|
||||||
else:
|
else:
|
||||||
# repo_url 为空时,也直接响应
|
# repo_url 为空时,也直接响应
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.db.models.subscribehistory import SubscribeHistory
|
|||||||
from app.db.models.user import User
|
from app.db.models.user import User
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.db.user_oper import get_current_active_user_async
|
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.scheduler import Scheduler
|
||||||
from app.schemas.types import MediaType, EventType, SystemConfigKey
|
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,
|
stype=stype,
|
||||||
page=page,
|
page=page,
|
||||||
count=count,
|
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,
|
subscribe_id=sub.subscribe_id,
|
||||||
share_title=sub.share_title,
|
share_title=sub.share_title,
|
||||||
share_comment=sub.share_comment,
|
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)
|
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
|
subscribe_in=schemas.Subscribe(**sub_dict), current_user=current_user
|
||||||
)
|
)
|
||||||
if result.success:
|
if result.success:
|
||||||
await SubscribeHelper().async_sub_fork(share_id=sub.id)
|
await MoviePilotServerHelper.async_sub_fork(share_id=sub.id)
|
||||||
return result
|
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,
|
name=name,
|
||||||
page=page,
|
page=page,
|
||||||
count=count,
|
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)
|
@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},
|
{"subscribe_id": subscribe_id, "subscribe_info": subscribe_info},
|
||||||
)
|
)
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
SubscribeHelper().sub_done_async(
|
MoviePilotServerHelper.sub_done_async(
|
||||||
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
{"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid}
|
||||||
)
|
)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|||||||
@@ -31,11 +31,10 @@ from app.db.user_oper import (
|
|||||||
)
|
)
|
||||||
from app.helper.image import ImageHelper
|
from app.helper.image import ImageHelper
|
||||||
from app.helper.message import MessageHelper
|
from app.helper.message import MessageHelper
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.progress import ProgressHelper
|
from app.helper.progress import ProgressHelper
|
||||||
from app.helper.rule import RuleHelper
|
from app.helper.rule import RuleHelper
|
||||||
from app.helper.subscribe import SubscribeHelper
|
|
||||||
from app.helper.system import SystemHelper
|
from app.helper.system import SystemHelper
|
||||||
from app.helper.usage import UsageHelper
|
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.scheduler import Scheduler
|
from app.scheduler import Scheduler
|
||||||
from app.schemas import ConfigChangeEventData
|
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
|
info["LLM_SUPPORT_AUDIO_OUTPUT"] = False
|
||||||
|
|
||||||
# 追加用户唯一ID和订阅分享管理权限
|
# 追加用户唯一ID和订阅分享管理权限
|
||||||
share_admin = SubscribeHelper().is_admin_user()
|
share_admin = MoviePilotServerHelper.is_admin_user()
|
||||||
info.update(
|
info.update(
|
||||||
{
|
{
|
||||||
"USER_UNIQUE_ID": SubscribeHelper().get_user_uuid(),
|
"USER_UNIQUE_ID": MoviePilotServerHelper.get_user_uuid(),
|
||||||
"SUBSCRIBE_SHARE_MANAGE": share_admin,
|
"SUBSCRIBE_SHARE_MANAGE": share_admin,
|
||||||
"WORKFLOW_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)
|
@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.models import Workflow
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.db.workflow_oper import WorkflowOper
|
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.scheduler import Scheduler
|
||||||
from app.schemas.types import EventType, EVENT_TYPE_NAMES
|
from app.schemas.types import EventType, EVENT_TYPE_NAMES
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ async def workflow_share(
|
|||||||
success=False, message="请填写工作流ID、分享标题和分享人"
|
success=False, message="请填写工作流ID、分享标题和分享人"
|
||||||
)
|
)
|
||||||
|
|
||||||
state, errmsg = await WorkflowHelper().async_workflow_share(
|
state, errmsg = await MoviePilotServerHelper.async_workflow_share_by_id(
|
||||||
workflow_id=workflow.id,
|
workflow_id=workflow.id,
|
||||||
share_title=workflow.share_title or "",
|
share_title=workflow.share_title or "",
|
||||||
share_comment=workflow.share_comment 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)
|
return schemas.Response(success=state, message=errmsg)
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ async def workflow_fork(
|
|||||||
|
|
||||||
# 更新复用次数
|
# 更新复用次数
|
||||||
if workflow:
|
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="复用成功")
|
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(
|
@router.post(
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ from app.core.module import ModuleManager
|
|||||||
from app.core.plugin import PluginManager
|
from app.core.plugin import PluginManager
|
||||||
from app.db.message_oper import MessageOper
|
from app.db.message_oper import MessageOper
|
||||||
from app.db.user_oper import UserOper
|
from app.db.user_oper import UserOper
|
||||||
from app.helper.recognize import MediaRecognizeShareHelper
|
|
||||||
from app.helper.message import MessageHelper, MessageQueueManager, MessageTemplateHelper
|
from app.helper.message import MessageHelper, MessageQueueManager, MessageTemplateHelper
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.service import ServiceConfigHelper
|
from app.helper.service import ServiceConfigHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import (
|
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]:
|
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||||
mtype = meta.type
|
mtype = meta.type
|
||||||
share_query_meta = share_meta or meta
|
share_query_meta = share_meta or meta
|
||||||
share_helper = MediaRecognizeShareHelper()
|
|
||||||
with fresh(not cache):
|
with fresh(not cache):
|
||||||
mediainfo = self.run_module(
|
mediainfo = self.run_module(
|
||||||
"recognize_media",
|
"recognize_media",
|
||||||
@@ -605,7 +604,7 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
)
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if not mediainfo.recognize_cache_hit:
|
if not mediainfo.recognize_cache_hit:
|
||||||
share_helper.report(
|
MoviePilotServerHelper.report_recognize_share(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
keyword_meta=share_query_meta,
|
keyword_meta=share_query_meta,
|
||||||
@@ -616,12 +615,12 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
share_query_meta, tmdbid, doubanid, bangumiid
|
share_query_meta, tmdbid, doubanid, bangumiid
|
||||||
):
|
):
|
||||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
||||||
shared_item = share_helper.query(
|
shared_item = MoviePilotServerHelper.query_recognize_share(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mtype=mtype,
|
mtype=mtype,
|
||||||
keyword_meta=share_query_meta,
|
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:
|
if shared_params:
|
||||||
with fresh(not cache):
|
with fresh(not cache):
|
||||||
mediainfo = self.run_module(
|
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]:
|
elif not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
|
||||||
mtype = meta.type
|
mtype = meta.type
|
||||||
share_query_meta = share_meta or meta
|
share_query_meta = share_meta or meta
|
||||||
share_helper = MediaRecognizeShareHelper()
|
|
||||||
async with async_fresh(not cache):
|
async with async_fresh(not cache):
|
||||||
mediainfo = await self.async_run_module(
|
mediainfo = await self.async_run_module(
|
||||||
"async_recognize_media",
|
"async_recognize_media",
|
||||||
@@ -690,7 +688,7 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
)
|
)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
if not mediainfo.recognize_cache_hit:
|
if not mediainfo.recognize_cache_hit:
|
||||||
await share_helper.async_report(
|
await MoviePilotServerHelper.async_report_recognize_share(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
keyword_meta=share_query_meta,
|
keyword_meta=share_query_meta,
|
||||||
@@ -701,12 +699,12 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
share_query_meta, tmdbid, doubanid, bangumiid
|
share_query_meta, tmdbid, doubanid, bangumiid
|
||||||
):
|
):
|
||||||
shared_cache_meta = self._snapshot_recognize_cache_meta(meta)
|
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,
|
meta=meta,
|
||||||
mtype=mtype,
|
mtype=mtype,
|
||||||
keyword_meta=share_query_meta,
|
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:
|
if shared_params:
|
||||||
async with async_fresh(not cache):
|
async with async_fresh(not cache):
|
||||||
mediainfo = await self.async_run_module(
|
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.site_oper import SiteOper
|
||||||
from app.db.subscribe_oper import SubscribeOper
|
from app.db.subscribe_oper import SubscribeOper
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
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.helper.torrent import TorrentHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import MediaRecognizeConvertEventData
|
from app.schemas import MediaRecognizeConvertEventData
|
||||||
@@ -709,7 +709,7 @@ class SubscribeChain(ChainBase):
|
|||||||
"mediainfo": mediainfo.to_dict(),
|
"mediainfo": mediainfo.to_dict(),
|
||||||
})
|
})
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
SubscribeHelper().sub_reg_async({
|
MoviePilotServerHelper.sub_reg_async({
|
||||||
"name": title,
|
"name": title,
|
||||||
"year": year,
|
"year": year,
|
||||||
"type": metainfo.type.value,
|
"type": metainfo.type.value,
|
||||||
@@ -890,7 +890,7 @@ class SubscribeChain(ChainBase):
|
|||||||
"mediainfo": mediainfo.to_dict(),
|
"mediainfo": mediainfo.to_dict(),
|
||||||
})
|
})
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
await SubscribeHelper().async_sub_reg({
|
await MoviePilotServerHelper.async_sub_reg({
|
||||||
"name": title,
|
"name": title,
|
||||||
"year": year,
|
"year": year,
|
||||||
"type": metainfo.type.value,
|
"type": metainfo.type.value,
|
||||||
@@ -1752,7 +1752,7 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.info(f'开始刷新follow用户分享订阅 ...')
|
logger.info(f'开始刷新follow用户分享订阅 ...')
|
||||||
success_count = 0
|
success_count = 0
|
||||||
subscribeoper = SubscribeOper()
|
subscribeoper = SubscribeOper()
|
||||||
for share_sub in SubscribeHelper().get_shares():
|
for share_sub in MoviePilotServerHelper.get_subscribe_shares():
|
||||||
if global_vars.is_system_stopped:
|
if global_vars.is_system_stopped:
|
||||||
break
|
break
|
||||||
uid = share_sub.get("share_uid")
|
uid = share_sub.get("share_uid")
|
||||||
@@ -2024,7 +2024,7 @@ class SubscribeChain(ChainBase):
|
|||||||
"mediainfo": mediainfo.to_dict(),
|
"mediainfo": mediainfo.to_dict(),
|
||||||
})
|
})
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
SubscribeHelper().sub_done_async({
|
MoviePilotServerHelper.sub_done_async({
|
||||||
"tmdbid": mediainfo.tmdb_id,
|
"tmdbid": mediainfo.tmdb_id,
|
||||||
"doubanid": mediainfo.douban_id
|
"doubanid": mediainfo.douban_id
|
||||||
})
|
})
|
||||||
@@ -2667,7 +2667,6 @@ class SubscribeChain(ChainBase):
|
|||||||
return False, "请输入至少一个有效的订阅 ID"
|
return False, "请输入至少一个有效的订阅 ID"
|
||||||
|
|
||||||
subscribeoper = SubscribeOper()
|
subscribeoper = SubscribeOper()
|
||||||
subscribehelper = SubscribeHelper()
|
|
||||||
deleted = []
|
deleted = []
|
||||||
missing = []
|
missing = []
|
||||||
for subscribe_id in subscribe_ids:
|
for subscribe_id in subscribe_ids:
|
||||||
@@ -2677,7 +2676,7 @@ class SubscribeChain(ChainBase):
|
|||||||
continue
|
continue
|
||||||
deleted.append(subscribe.name)
|
deleted.append(subscribe.name)
|
||||||
subscribeoper.delete(subscribe_id)
|
subscribeoper.delete(subscribe_id)
|
||||||
subscribehelper.sub_done_async(
|
MoviePilotServerHelper.sub_done_async(
|
||||||
{
|
{
|
||||||
"tmdbid": subscribe.tmdbid,
|
"tmdbid": subscribe.tmdbid,
|
||||||
"doubanid": subscribe.doubanid,
|
"doubanid": subscribe.doubanid,
|
||||||
@@ -2706,7 +2705,6 @@ class SubscribeChain(ChainBase):
|
|||||||
return
|
return
|
||||||
arg_strs = str(arg_str).split()
|
arg_strs = str(arg_str).split()
|
||||||
subscribeoper = SubscribeOper()
|
subscribeoper = SubscribeOper()
|
||||||
subscribehelper = SubscribeHelper()
|
|
||||||
for arg_str in arg_strs:
|
for arg_str in arg_strs:
|
||||||
arg_str = arg_str.strip()
|
arg_str = arg_str.strip()
|
||||||
if not arg_str.isdigit():
|
if not arg_str.isdigit():
|
||||||
@@ -2720,7 +2718,7 @@ class SubscribeChain(ChainBase):
|
|||||||
# 删除订阅
|
# 删除订阅
|
||||||
subscribeoper.delete(subscribe_id)
|
subscribeoper.delete(subscribe_id)
|
||||||
# 统计订阅
|
# 统计订阅
|
||||||
subscribehelper.sub_done_async({
|
MoviePilotServerHelper.sub_done_async({
|
||||||
"tmdbid": subscribe.tmdbid,
|
"tmdbid": subscribe.tmdbid,
|
||||||
"doubanid": subscribe.doubanid
|
"doubanid": subscribe.doubanid
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from app.core.config import settings
|
|||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
from app.db.plugindata_oper import PluginDataOper
|
from app.db.plugindata_oper import PluginDataOper
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.plugin import PluginHelper
|
from app.helper.plugin import PluginHelper
|
||||||
from app.helper.sites import SitesHelper # noqa
|
from app.helper.sites import SitesHelper # noqa
|
||||||
from app.log import logger
|
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)
|
state, msg = PluginHelper().install(pid=plugin.id, repo_url=plugin.repo_url, force_install=True)
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
if state:
|
if state:
|
||||||
|
MoviePilotServerHelper.install_plugin_reg(plugin_id=plugin.id, repo_url=plugin.repo_url)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"插件 {plugin.plugin_name} 安装成功,版本:{plugin.plugin_version},耗时:{elapsed_time:.2f} 秒")
|
f"插件 {plugin.plugin_name} 安装成功,版本:{plugin.plugin_version},耗时:{elapsed_time:.2f} 秒")
|
||||||
sync_plugins.append(plugin.id)
|
sync_plugins.append(plugin.id)
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_base_url = "https://raw.githubusercontent.com/{user}/{repo}/main/"
|
_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 子进程和导入缓存刷新互相踩踏。
|
||||||
_pip_install_lock = threading.Lock()
|
_pip_install_lock = threading.Lock()
|
||||||
# 这些包一旦被插件覆盖,最容易直接拖垮主程序启动,因此冲突提示需要单独高亮。
|
# 这些包一旦被插件覆盖,最容易直接拖垮主程序启动,因此冲突提示需要单独高亮。
|
||||||
@@ -72,10 +69,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.systemconfig = SystemConfigOper()
|
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
|
@staticmethod
|
||||||
def is_local_repo_url(repo_url: Optional[str]) -> bool:
|
def is_local_repo_url(repo_url: Optional[str]) -> bool:
|
||||||
@@ -147,25 +140,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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
|
@staticmethod
|
||||||
def get_current_system_version() -> Optional[Version]:
|
def get_current_system_version() -> Optional[Version]:
|
||||||
"""
|
"""
|
||||||
@@ -505,65 +479,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
return None, None
|
return None, None
|
||||||
return user, repo
|
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) \
|
def install(self, pid: str, repo_url: str, package_version: Optional[str] = None, force_install: bool = False) \
|
||||||
-> Tuple[bool, str]:
|
-> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
@@ -1595,7 +1510,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
||||||
return False, dep_msg
|
return False, dep_msg
|
||||||
|
|
||||||
self.install_reg(pid, repo_url)
|
|
||||||
self.refresh_persistent_plugin_backup(pid)
|
self.refresh_persistent_plugin_backup(pid)
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
@@ -1961,64 +1875,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
return None
|
return None
|
||||||
return self.__parse_plugin_index_response(res.text)
|
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) -> \
|
async def __async_get_file_list(self, pid: str, user_repo: str, package_version: Optional[str] = None) -> \
|
||||||
Tuple[Optional[list], Optional[str]]:
|
Tuple[Optional[list], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
@@ -2503,7 +2359,6 @@ class PluginHelper(metaclass=WeakSingleton):
|
|||||||
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
logger.warn(f"{pid} 已清理对应插件目录,请尝试重新安装")
|
||||||
return False, dep_msg
|
return False, dep_msg
|
||||||
|
|
||||||
await self.async_install_reg(pid, repo_url)
|
|
||||||
await asyncio.to_thread(self.refresh_persistent_plugin_backup, pid)
|
await asyncio.to_thread(self.refresh_persistent_plugin_backup, pid)
|
||||||
return True, ""
|
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"),
|
|
||||||
}
|
|
||||||
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 ""
|
|
||||||
+2
-2
@@ -36,7 +36,7 @@ from app.db.systemconfig_oper import SystemConfigOper
|
|||||||
from app.helper.image import WallpaperHelper
|
from app.helper.image import WallpaperHelper
|
||||||
from app.helper.message import MessageHelper
|
from app.helper.message import MessageHelper
|
||||||
from app.helper.sites import SitesHelper # noqa
|
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.log import logger
|
||||||
from app.schemas import Notification, NotificationType, Workflow
|
from app.schemas import Notification, NotificationType, Workflow
|
||||||
from app.schemas.types import EventType, SystemConfigKey
|
from app.schemas.types import EventType, SystemConfigKey
|
||||||
@@ -405,7 +405,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
|
|||||||
},
|
},
|
||||||
"usage_report": {
|
"usage_report": {
|
||||||
"name": "安装版本统计上报",
|
"name": "安装版本统计上报",
|
||||||
"func": UsageHelper().report,
|
"func": MoviePilotServerHelper.report_usage,
|
||||||
"running": False,
|
"running": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from fastapi import FastAPI
|
|||||||
|
|
||||||
from app.chain.system import SystemChain
|
from app.chain.system import SystemChain
|
||||||
from app.core.config import global_vars
|
from app.core.config import global_vars
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.helper.system import SystemHelper
|
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.command_initializer import init_command, stop_command, restart_command
|
||||||
from app.startup.modules_initializer import init_modules, stop_modules
|
from app.startup.modules_initializer import init_modules, stop_modules
|
||||||
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
||||||
@@ -31,7 +31,7 @@ async def init_extra():
|
|||||||
# 重启完成
|
# 重启完成
|
||||||
SystemChain().restart_finish()
|
SystemChain().restart_finish()
|
||||||
# 上报当前安装版本
|
# 上报当前安装版本
|
||||||
await UsageHelper().async_report()
|
await MoviePilotServerHelper.async_report_usage()
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from app.helper.display import DisplayHelper
|
|||||||
from app.helper.doh import DohHelper
|
from app.helper.doh import DohHelper
|
||||||
from app.helper.resource import ResourceHelper
|
from app.helper.resource import ResourceHelper
|
||||||
from app.helper.message import MessageHelper, stop_message
|
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 import close_database
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.command import CommandChain
|
from app.command import CommandChain
|
||||||
@@ -152,8 +152,11 @@ def init_modules():
|
|||||||
ModuleManager()
|
ModuleManager()
|
||||||
# 启动事件消费
|
# 启动事件消费
|
||||||
EventManager().start()
|
EventManager().start()
|
||||||
# 初始化订阅分享
|
# 初始化共享服务端状态
|
||||||
SubscribeHelper()
|
MoviePilotServerHelper.init_plugin_report()
|
||||||
|
MoviePilotServerHelper.init_subscribe_report()
|
||||||
|
MoviePilotServerHelper.get_user_uuid()
|
||||||
|
MoviePilotServerHelper.get_github_user()
|
||||||
# 初始化AI智能体
|
# 初始化AI智能体
|
||||||
init_agent()
|
init_agent()
|
||||||
# 启动前端服务
|
# 启动前端服务
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ class TestAgentPluginTools(unittest.TestCase):
|
|||||||
plugin_manager = MagicMock()
|
plugin_manager = MagicMock()
|
||||||
plugin_manager.get_plugin_ids.return_value = ["DemoPlugin"]
|
plugin_manager.get_plugin_ids.return_value = ["DemoPlugin"]
|
||||||
plugin_helper = MagicMock()
|
plugin_helper = MagicMock()
|
||||||
plugin_helper.async_install_reg = AsyncMock(return_value=True)
|
|
||||||
config_oper = MagicMock()
|
config_oper = MagicMock()
|
||||||
config_oper.get.return_value = ["DemoPlugin"]
|
config_oper.get.return_value = ["DemoPlugin"]
|
||||||
calls = []
|
calls = []
|
||||||
@@ -196,6 +195,9 @@ class TestAgentPluginTools(unittest.TestCase):
|
|||||||
), patch(
|
), patch(
|
||||||
"app.agent.tools.impl._plugin_tool_utils.reload_plugin_runtime",
|
"app.agent.tools.impl._plugin_tool_utils.reload_plugin_runtime",
|
||||||
) as reload_runtime, patch(
|
) as reload_runtime, patch(
|
||||||
|
"app.agent.tools.impl._plugin_tool_utils.MoviePilotServerHelper.async_install_plugin_reg",
|
||||||
|
AsyncMock(return_value=True),
|
||||||
|
) as install_reg, patch(
|
||||||
"app.agent.tools.impl._plugin_tool_utils.asyncio.to_thread",
|
"app.agent.tools.impl._plugin_tool_utils.asyncio.to_thread",
|
||||||
side_effect=fake_to_thread,
|
side_effect=fake_to_thread,
|
||||||
):
|
):
|
||||||
@@ -210,8 +212,8 @@ class TestAgentPluginTools(unittest.TestCase):
|
|||||||
self.assertTrue(success)
|
self.assertTrue(success)
|
||||||
self.assertEqual("插件已存在,已刷新加载", message)
|
self.assertEqual("插件已存在,已刷新加载", message)
|
||||||
self.assertTrue(refreshed_only)
|
self.assertTrue(refreshed_only)
|
||||||
plugin_helper.async_install_reg.assert_awaited_once_with(
|
install_reg.assert_awaited_once_with(
|
||||||
pid="DemoPlugin",
|
plugin_id="DemoPlugin",
|
||||||
repo_url="https://example.com/market",
|
repo_url="https://example.com/market",
|
||||||
)
|
)
|
||||||
self.assertEqual(1, len(calls))
|
self.assertEqual(1, len(calls))
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ def test_recognize_media_uses_meta_episode_group():
|
|||||||
mediainfo = MediaInfo(title="测试剧集", year="2024", tmdb_id=100, type=MediaType.TV)
|
mediainfo = MediaInfo(title="测试剧集", year="2024", tmdb_id=100, type=MediaType.TV)
|
||||||
|
|
||||||
with patch.object(chain, "run_module", return_value=mediainfo) as run_module, patch(
|
with patch.object(chain, "run_module", return_value=mediainfo) as run_module, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
), patch("app.chain.MediaRecognizeShareHelper.query") as query_mock:
|
), patch("app.chain.MoviePilotServerHelper.query_recognize_share") as query_mock:
|
||||||
result = chain.recognize_media(meta=meta, cache=False)
|
result = chain.recognize_media(meta=meta, cache=False)
|
||||||
|
|
||||||
assert result is mediainfo
|
assert result is mediainfo
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from app.core.context import MediaInfo
|
|||||||
from app.core.meta import MetaBase
|
from app.core.meta import MetaBase
|
||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
from app.helper.recognize import MediaRecognizeShareHelper
|
from app.helper.server import MoviePilotServerHelper
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
|
|
||||||
|
|
||||||
@@ -43,16 +43,16 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
mediainfo = MediaInfo(title="测试电影", year="2024", tmdb_id=100, type=MediaType.MOVIE)
|
mediainfo = MediaInfo(title="测试电影", year="2024", tmdb_id=100, type=MediaType.MOVIE)
|
||||||
|
|
||||||
with patch.object(self.chain, "run_module", return_value=mediainfo) as run_module, patch(
|
with patch.object(self.chain, "run_module", return_value=mediainfo) as run_module, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as report_mock, patch(
|
) as report_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.query"
|
"app.chain.MoviePilotServerHelper.query_recognize_share"
|
||||||
) as query_mock:
|
) as query_mock:
|
||||||
result = self.chain.recognize_media(meta=meta, cache=False)
|
result = self.chain.recognize_media(meta=meta, cache=False)
|
||||||
|
|
||||||
self.assertIs(result, mediainfo)
|
self.assertIs(result, mediainfo)
|
||||||
run_module.assert_called_once()
|
run_module.assert_called_once()
|
||||||
report_mock.assert_called_once_with(meta=meta, mediainfo=mediainfo)
|
report_mock.assert_called_once_with(meta=meta, mediainfo=mediainfo, keyword_meta=meta)
|
||||||
query_mock.assert_not_called()
|
query_mock.assert_not_called()
|
||||||
|
|
||||||
def test_query_shared_result_when_local_recognize_failed(self):
|
def test_query_shared_result_when_local_recognize_failed(self):
|
||||||
@@ -67,10 +67,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"run_module",
|
"run_module",
|
||||||
side_effect=[None, shared_media],
|
side_effect=[None, shared_media],
|
||||||
) as run_module, patch(
|
) as run_module, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.query",
|
"app.chain.MoviePilotServerHelper.query_recognize_share",
|
||||||
return_value={"type": "tv", "tmdbid": 200, "season": 1},
|
return_value={"type": "tv", "tmdbid": 200, "season": 1},
|
||||||
) as query_mock, patch(
|
) as query_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.to_recognize_params",
|
"app.chain.MoviePilotServerHelper.to_recognize_params",
|
||||||
return_value={
|
return_value={
|
||||||
"mtype": MediaType.TV,
|
"mtype": MediaType.TV,
|
||||||
"tmdbid": 200,
|
"tmdbid": 200,
|
||||||
@@ -79,7 +79,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"season": 1,
|
"season": 1,
|
||||||
},
|
},
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=False,
|
return_value=False,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
self.chain,
|
self.chain,
|
||||||
@@ -89,7 +89,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIs(result, shared_media)
|
self.assertIs(result, shared_media)
|
||||||
self.assertEqual(run_module.call_count, 2)
|
self.assertEqual(run_module.call_count, 2)
|
||||||
query_mock.assert_called_once_with(meta=meta, mtype=None)
|
query_mock.assert_called_once_with(meta=meta, mtype=None, keyword_meta=meta)
|
||||||
second_call = run_module.call_args_list[1]
|
second_call = run_module.call_args_list[1]
|
||||||
self.assertEqual(second_call.kwargs["tmdbid"], 200)
|
self.assertEqual(second_call.kwargs["tmdbid"], 200)
|
||||||
self.assertEqual(second_call.kwargs["mtype"], MediaType.TV)
|
self.assertEqual(second_call.kwargs["mtype"], MediaType.TV)
|
||||||
@@ -109,10 +109,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"async_run_module",
|
"async_run_module",
|
||||||
async_run_module,
|
async_run_module,
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.async_query",
|
"app.chain.MoviePilotServerHelper.async_query_recognize_share",
|
||||||
AsyncMock(return_value={"type": "tv", "tmdbid": 300, "season": 2}),
|
AsyncMock(return_value={"type": "tv", "tmdbid": 300, "season": 2}),
|
||||||
) as query_mock, patch(
|
) as query_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.to_recognize_params",
|
"app.chain.MoviePilotServerHelper.to_recognize_params",
|
||||||
return_value={
|
return_value={
|
||||||
"mtype": MediaType.TV,
|
"mtype": MediaType.TV,
|
||||||
"tmdbid": 300,
|
"tmdbid": 300,
|
||||||
@@ -121,7 +121,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"season": 2,
|
"season": 2,
|
||||||
},
|
},
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.async_report",
|
"app.chain.MoviePilotServerHelper.async_report_recognize_share",
|
||||||
AsyncMock(return_value=False),
|
AsyncMock(return_value=False),
|
||||||
), patch.object(
|
), patch.object(
|
||||||
self.chain,
|
self.chain,
|
||||||
@@ -135,7 +135,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIs(result, shared_media)
|
self.assertIs(result, shared_media)
|
||||||
self.assertEqual(async_run_module.await_count, 2)
|
self.assertEqual(async_run_module.await_count, 2)
|
||||||
query_mock.assert_awaited_once_with(meta=meta, mtype=None)
|
query_mock.assert_awaited_once_with(meta=meta, mtype=None, keyword_meta=meta)
|
||||||
backfill_mock.assert_awaited_once()
|
backfill_mock.assert_awaited_once()
|
||||||
self.assertIsNone(meta.begin_season)
|
self.assertIsNone(meta.begin_season)
|
||||||
|
|
||||||
@@ -158,10 +158,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"run_module",
|
"run_module",
|
||||||
side_effect=[None, shared_media, None],
|
side_effect=[None, shared_media, None],
|
||||||
) as run_module_mock, patch(
|
) as run_module_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.query",
|
"app.chain.MoviePilotServerHelper.query_recognize_share",
|
||||||
return_value={"type": "movie", "tmdbid": 700},
|
return_value={"type": "movie", "tmdbid": 700},
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.to_recognize_params",
|
"app.chain.MoviePilotServerHelper.to_recognize_params",
|
||||||
return_value={
|
return_value={
|
||||||
"mtype": MediaType.MOVIE,
|
"mtype": MediaType.MOVIE,
|
||||||
"tmdbid": 700,
|
"tmdbid": 700,
|
||||||
@@ -170,7 +170,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"season": None,
|
"season": None,
|
||||||
},
|
},
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=False,
|
return_value=False,
|
||||||
):
|
):
|
||||||
result = self.chain.recognize_media(meta=meta, cache=False)
|
result = self.chain.recognize_media(meta=meta, cache=False)
|
||||||
@@ -188,7 +188,6 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
查询和上报共享识别时应优先使用未应用识别词的识别名称
|
查询和上报共享识别时应优先使用未应用识别词的识别名称
|
||||||
"""
|
"""
|
||||||
helper = MediaRecognizeShareHelper()
|
|
||||||
meta = self._build_meta("应用识别词后的名称", MediaType.TV)
|
meta = self._build_meta("应用识别词后的名称", MediaType.TV)
|
||||||
meta.original_name = "未应用识别词的名称"
|
meta.original_name = "未应用识别词的名称"
|
||||||
meta.year = "2024"
|
meta.year = "2024"
|
||||||
@@ -201,8 +200,8 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
season=1,
|
season=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
query_params = helper._build_query_params(meta=meta)
|
query_params = MoviePilotServerHelper._build_recognize_query_params(meta=meta)
|
||||||
report_payload = helper._build_report_payload(meta=meta, mediainfo=mediainfo)
|
report_payload = MoviePilotServerHelper._build_recognize_report_payload(meta=meta, mediainfo=mediainfo)
|
||||||
|
|
||||||
self.assertEqual(query_params["keyword"], "未应用识别词的名称")
|
self.assertEqual(query_params["keyword"], "未应用识别词的名称")
|
||||||
self.assertEqual(report_payload["keyword"], "未应用识别词的名称")
|
self.assertEqual(report_payload["keyword"], "未应用识别词的名称")
|
||||||
@@ -211,7 +210,6 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
共享识别应允许用原始关键字上报,同时保留辅助识别后的年份/季信息。
|
共享识别应允许用原始关键字上报,同时保留辅助识别后的年份/季信息。
|
||||||
"""
|
"""
|
||||||
helper = MediaRecognizeShareHelper()
|
|
||||||
meta = self._build_meta("辅助识别后的名称", MediaType.TV)
|
meta = self._build_meta("辅助识别后的名称", MediaType.TV)
|
||||||
meta.year = "2024"
|
meta.year = "2024"
|
||||||
meta.begin_season = 2
|
meta.begin_season = 2
|
||||||
@@ -227,12 +225,12 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
season=2,
|
season=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
query_params = helper._build_query_params(
|
query_params = MoviePilotServerHelper._build_recognize_query_params(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mtype=None,
|
mtype=None,
|
||||||
keyword_meta=keyword_meta,
|
keyword_meta=keyword_meta,
|
||||||
)
|
)
|
||||||
report_payload = helper._build_report_payload(
|
report_payload = MoviePilotServerHelper._build_recognize_report_payload(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
keyword_meta=keyword_meta,
|
keyword_meta=keyword_meta,
|
||||||
@@ -257,7 +255,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
mediainfo = MediaInfo(title="测试剧集", year="2024", tmdb_id=402, type=MediaType.TV)
|
mediainfo = MediaInfo(title="测试剧集", year="2024", tmdb_id=402, type=MediaType.TV)
|
||||||
|
|
||||||
with patch.object(self.chain, "run_module", return_value=mediainfo), patch(
|
with patch.object(self.chain, "run_module", return_value=mediainfo), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as report_mock:
|
) as report_mock:
|
||||||
result = self.chain.recognize_media(meta=meta, share_meta=share_meta, cache=False)
|
result = self.chain.recognize_media(meta=meta, share_meta=share_meta, cache=False)
|
||||||
@@ -284,10 +282,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"run_module",
|
"run_module",
|
||||||
side_effect=[None, shared_media],
|
side_effect=[None, shared_media],
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.query",
|
"app.chain.MoviePilotServerHelper.query_recognize_share",
|
||||||
return_value={"type": "tv", "tmdbid": 403, "season": 1},
|
return_value={"type": "tv", "tmdbid": 403, "season": 1},
|
||||||
) as query_mock, patch(
|
) as query_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.to_recognize_params",
|
"app.chain.MoviePilotServerHelper.to_recognize_params",
|
||||||
return_value={
|
return_value={
|
||||||
"mtype": MediaType.TV,
|
"mtype": MediaType.TV,
|
||||||
"tmdbid": 403,
|
"tmdbid": 403,
|
||||||
@@ -296,7 +294,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"season": 1,
|
"season": 1,
|
||||||
},
|
},
|
||||||
), patch(
|
), patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=False,
|
return_value=False,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
self.chain,
|
self.chain,
|
||||||
@@ -311,7 +309,7 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
self.assertIs(result, shared_media)
|
self.assertIs(result, shared_media)
|
||||||
query_mock.assert_called_once_with(
|
query_mock.assert_called_once_with(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mtype=None,
|
mtype=MediaType.TV,
|
||||||
keyword_meta=share_meta,
|
keyword_meta=share_meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -324,10 +322,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
mediainfo.recognize_cache_hit = True
|
mediainfo.recognize_cache_hit = True
|
||||||
|
|
||||||
with patch.object(self.chain, "run_module", return_value=mediainfo) as run_module, patch(
|
with patch.object(self.chain, "run_module", return_value=mediainfo) as run_module, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.report",
|
"app.chain.MoviePilotServerHelper.report_recognize_share",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as report_mock, patch(
|
) as report_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.query"
|
"app.chain.MoviePilotServerHelper.query_recognize_share"
|
||||||
) as query_mock:
|
) as query_mock:
|
||||||
result = self.chain.recognize_media(meta=meta)
|
result = self.chain.recognize_media(meta=meta)
|
||||||
|
|
||||||
@@ -350,10 +348,10 @@ class TestMediaRecognizeShare(unittest.TestCase):
|
|||||||
"async_run_module",
|
"async_run_module",
|
||||||
AsyncMock(return_value=mediainfo),
|
AsyncMock(return_value=mediainfo),
|
||||||
) as async_run_module, patch(
|
) as async_run_module, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.async_report",
|
"app.chain.MoviePilotServerHelper.async_report_recognize_share",
|
||||||
AsyncMock(return_value=True),
|
AsyncMock(return_value=True),
|
||||||
) as report_mock, patch(
|
) as report_mock, patch(
|
||||||
"app.chain.MediaRecognizeShareHelper.async_query",
|
"app.chain.MoviePilotServerHelper.async_query_recognize_share",
|
||||||
AsyncMock(),
|
AsyncMock(),
|
||||||
) as query_mock:
|
) as query_mock:
|
||||||
result = await self.chain.async_recognize_media(meta=meta)
|
result = await self.chain.async_recognize_media(meta=meta)
|
||||||
|
|||||||
@@ -14,23 +14,29 @@ from packaging.version import Version
|
|||||||
|
|
||||||
class PluginHelperTest(TestCase):
|
class PluginHelperTest(TestCase):
|
||||||
|
|
||||||
def test_sanitize_repo_url_for_statistic_keeps_remote_url(self):
|
def test_sanitize_plugin_repo_url_keeps_remote_url(self):
|
||||||
|
"""
|
||||||
|
插件安装统计脱敏保留远端仓库地址。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from app.helper.plugin import PluginHelper
|
from app.helper.server import MoviePilotServerHelper
|
||||||
except ModuleNotFoundError as exc:
|
except ModuleNotFoundError as exc:
|
||||||
self.skipTest(f"missing dependency: {exc}")
|
self.skipTest(f"missing dependency: {exc}")
|
||||||
repo_url = "https://github.com/InfinityPacer/MoviePilot-Plugins"
|
repo_url = "https://github.com/InfinityPacer/MoviePilot-Plugins"
|
||||||
self.assertEqual(repo_url, PluginHelper.sanitize_repo_url_for_statistic(repo_url))
|
self.assertEqual(repo_url, MoviePilotServerHelper.sanitize_plugin_repo_url(repo_url))
|
||||||
|
|
||||||
def test_sanitize_repo_url_for_statistic_strips_local_path(self):
|
def test_sanitize_plugin_repo_url_strips_local_path(self):
|
||||||
|
"""
|
||||||
|
插件安装统计脱敏移除本地仓库绝对路径。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from app.helper.plugin import PluginHelper
|
from app.helper.server import MoviePilotServerHelper
|
||||||
except ModuleNotFoundError as exc:
|
except ModuleNotFoundError as exc:
|
||||||
self.skipTest(f"missing dependency: {exc}")
|
self.skipTest(f"missing dependency: {exc}")
|
||||||
repo_url = "local://TestPlugin?path=/Users/InfinityPacer/GitHub/MoviePilot/MoviePilot-Plugins&version=v2"
|
repo_url = "local://TestPlugin?path=/Users/InfinityPacer/GitHub/MoviePilot/MoviePilot-Plugins&version=v2"
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"local://TestPlugin?version=v2",
|
"local://TestPlugin?version=v2",
|
||||||
PluginHelper.sanitize_repo_url_for_statistic(repo_url)
|
MoviePilotServerHelper.sanitize_plugin_repo_url(repo_url)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_append_cache_buster_only_during_fresh_context(self):
|
def test_append_cache_buster_only_during_fresh_context(self):
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app.helper.server import MoviePilotServerHelper
|
||||||
|
|
||||||
|
|
||||||
|
class MoviePilotServerHelperTests(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
MoviePilot 服务端请求辅助工具测试。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""
|
||||||
|
清理安装用户 ID 缓存,避免不同用例之间互相影响。
|
||||||
|
"""
|
||||||
|
MoviePilotServerHelper._user_uid = None
|
||||||
|
|
||||||
|
def test_server_request_adds_user_uid_header(self):
|
||||||
|
"""
|
||||||
|
发往 MoviePilot 服务端的请求会自动携带安装用户 ID。
|
||||||
|
"""
|
||||||
|
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
|
||||||
|
patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
|
||||||
|
headers = MoviePilotServerHelper.build_headers(
|
||||||
|
"https://movie-pilot.org/plugin/install",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(headers["X-MoviePilot-User-Uid"], "uid-1")
|
||||||
|
self.assertEqual(headers["Content-Type"], "application/json")
|
||||||
|
|
||||||
|
def test_non_server_request_does_not_add_user_uid_header(self):
|
||||||
|
"""
|
||||||
|
发往其他域名的请求不会携带安装用户 ID。
|
||||||
|
"""
|
||||||
|
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
|
||||||
|
patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
|
||||||
|
headers = MoviePilotServerHelper.build_headers(
|
||||||
|
"https://example.com/plugin/install",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn("X-MoviePilot-User-Uid", headers)
|
||||||
|
|
||||||
|
def test_existing_user_uid_header_is_preserved(self):
|
||||||
|
"""
|
||||||
|
调用方显式传入的安装用户 ID 请求头不被覆盖。
|
||||||
|
"""
|
||||||
|
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
|
||||||
|
patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
|
||||||
|
headers = MoviePilotServerHelper.build_headers(
|
||||||
|
"https://movie-pilot.org/plugin/install",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-MoviePilot-User-Uid": "custom-uid",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(headers["X-MoviePilot-User-Uid"], "custom-uid")
|
||||||
|
|
||||||
|
def test_existing_user_uid_header_is_detected_case_insensitively(self):
|
||||||
|
"""
|
||||||
|
调用方使用不同大小写的安装用户 ID 请求头时不会重复注入。
|
||||||
|
"""
|
||||||
|
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
|
||||||
|
patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
|
||||||
|
headers = MoviePilotServerHelper.build_headers(
|
||||||
|
"https://movie-pilot.org/plugin/install",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-moviepilot-user-uid": "custom-uid",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn("X-MoviePilot-User-Uid", headers)
|
||||||
|
self.assertEqual(headers["x-moviepilot-user-uid"], "custom-uid")
|
||||||
|
|
||||||
|
def test_content_type_can_be_added(self):
|
||||||
|
"""
|
||||||
|
构建 JSON 请求头时会补充 Content-Type。
|
||||||
|
"""
|
||||||
|
with patch.object(MoviePilotServerHelper, "get_user_uid", return_value="uid-1"), \
|
||||||
|
patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"):
|
||||||
|
headers = MoviePilotServerHelper.build_headers(
|
||||||
|
"https://movie-pilot.org/plugin/install",
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(headers["Content-Type"], "application/json")
|
||||||
|
|
||||||
|
def test_subscribe_fork_uses_fork_endpoint(self):
|
||||||
|
"""
|
||||||
|
订阅复用请求使用服务端 fork 接口。
|
||||||
|
"""
|
||||||
|
with patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"), \
|
||||||
|
patch.object(MoviePilotServerHelper, "_get", return_value=None) as request:
|
||||||
|
MoviePilotServerHelper.subscribe_fork(9)
|
||||||
|
|
||||||
|
request.assert_called_once_with(
|
||||||
|
"https://movie-pilot.org/subscribe/fork/9",
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_fork_uses_fork_endpoint(self):
|
||||||
|
"""
|
||||||
|
工作流复用请求使用服务端 fork 接口。
|
||||||
|
"""
|
||||||
|
with patch("app.helper.server.settings.MP_SERVER_HOST", "https://movie-pilot.org"), \
|
||||||
|
patch.object(MoviePilotServerHelper, "_get", return_value=None) as request:
|
||||||
|
MoviePilotServerHelper.workflow_fork(9)
|
||||||
|
|
||||||
|
request.assert_called_once_with(
|
||||||
|
"https://movie-pilot.org/workflow/fork/9",
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -187,17 +187,38 @@ def _load_subscribe_chain_class():
|
|||||||
|
|
||||||
logger_module.logger = _Logger()
|
logger_module.logger = _Logger()
|
||||||
|
|
||||||
helper_subscribe_module = ensure_module("app.helper.subscribe", types.ModuleType("app.helper.subscribe"))
|
helper_server_module = ensure_module("app.helper.server", types.ModuleType("app.helper.server"))
|
||||||
|
|
||||||
class _SubscribeHelper:
|
class _MoviePilotServerHelper:
|
||||||
def sub_done_async(self, *args, **kwargs):
|
@staticmethod
|
||||||
|
def sub_done_async(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
忽略订阅完成统计上报。
|
||||||
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_shares():
|
def sub_reg_async(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
忽略订阅新增统计上报。
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def async_sub_reg(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
忽略异步订阅新增统计上报。
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_subscribe_shares():
|
||||||
|
"""
|
||||||
|
返回空的订阅共享数据。
|
||||||
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
helper_subscribe_module.SubscribeHelper = _SubscribeHelper
|
helper_server_module.MoviePilotServerHelper = _MoviePilotServerHelper
|
||||||
|
|
||||||
helper_torrent_module = ensure_module("app.helper.torrent", types.ModuleType("app.helper.torrent"))
|
helper_torrent_module = ensure_module("app.helper.torrent", types.ModuleType("app.helper.torrent"))
|
||||||
helper_torrent_module.TorrentHelper = type("TorrentHelper", (), {})
|
helper_torrent_module.TorrentHelper = type("TorrentHelper", (), {})
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ _ORIGINAL_STUBBED_MODULES = {}
|
|||||||
|
|
||||||
|
|
||||||
def _stub_module(name: str, **attrs):
|
def _stub_module(name: str, **attrs):
|
||||||
|
"""
|
||||||
|
安装临时 stub 模块,并记录原模块用于导入后恢复。
|
||||||
|
"""
|
||||||
if name not in _ORIGINAL_STUBBED_MODULES:
|
if name not in _ORIGINAL_STUBBED_MODULES:
|
||||||
_ORIGINAL_STUBBED_MODULES[name] = sys.modules.get(name)
|
_ORIGINAL_STUBBED_MODULES[name] = sys.modules.get(name)
|
||||||
module = sys.modules.get(name)
|
module = ModuleType(name)
|
||||||
if module is None:
|
|
||||||
module = ModuleType(name)
|
|
||||||
sys.modules[name] = module
|
|
||||||
for key, value in attrs.items():
|
for key, value in attrs.items():
|
||||||
setattr(module, key, value)
|
setattr(module, key, value)
|
||||||
|
sys.modules[name] = module
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ _stub_module("app.helper.mediaserver", MediaServerHelper=_Dummy)
|
|||||||
_stub_module("app.helper.message", MessageHelper=_Dummy)
|
_stub_module("app.helper.message", MessageHelper=_Dummy)
|
||||||
_stub_module("app.helper.progress", ProgressHelper=_Dummy)
|
_stub_module("app.helper.progress", ProgressHelper=_Dummy)
|
||||||
_stub_module("app.helper.rule", RuleHelper=_Dummy)
|
_stub_module("app.helper.rule", RuleHelper=_Dummy)
|
||||||
_stub_module("app.helper.subscribe", SubscribeHelper=_Dummy)
|
_stub_module("app.helper.server", MoviePilotServerHelper=_Dummy)
|
||||||
_stub_module("app.helper.system", SystemHelper=_Dummy)
|
_stub_module("app.helper.system", SystemHelper=_Dummy)
|
||||||
_stub_module("app.helper.image", ImageHelper=_Dummy)
|
_stub_module("app.helper.image", ImageHelper=_Dummy)
|
||||||
_stub_module("app.scheduler", Scheduler=_Dummy)
|
_stub_module("app.scheduler", Scheduler=_Dummy)
|
||||||
|
|||||||
@@ -6,13 +6,19 @@ from types import ModuleType, SimpleNamespace
|
|||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
|
||||||
|
_ORIGINAL_STUBBED_MODULES = {}
|
||||||
|
|
||||||
|
|
||||||
def _stub_module(name: str, **attrs):
|
def _stub_module(name: str, **attrs):
|
||||||
module = sys.modules.get(name)
|
"""
|
||||||
if module is None:
|
安装临时 stub 模块,并记录原模块用于导入后恢复。
|
||||||
module = ModuleType(name)
|
"""
|
||||||
sys.modules[name] = module
|
if name not in _ORIGINAL_STUBBED_MODULES:
|
||||||
|
_ORIGINAL_STUBBED_MODULES[name] = sys.modules.get(name)
|
||||||
|
module = ModuleType(name)
|
||||||
for key, value in attrs.items():
|
for key, value in attrs.items():
|
||||||
setattr(module, key, value)
|
setattr(module, key, value)
|
||||||
|
sys.modules[name] = module
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ _stub_module("app.helper.mediaserver", MediaServerHelper=_Dummy)
|
|||||||
_stub_module("app.helper.message", MessageHelper=_Dummy)
|
_stub_module("app.helper.message", MessageHelper=_Dummy)
|
||||||
_stub_module("app.helper.progress", ProgressHelper=_Dummy)
|
_stub_module("app.helper.progress", ProgressHelper=_Dummy)
|
||||||
_stub_module("app.helper.rule", RuleHelper=_Dummy)
|
_stub_module("app.helper.rule", RuleHelper=_Dummy)
|
||||||
_stub_module("app.helper.subscribe", SubscribeHelper=_Dummy)
|
_stub_module("app.helper.server", MoviePilotServerHelper=_Dummy)
|
||||||
_stub_module("app.helper.system", SystemHelper=_Dummy)
|
_stub_module("app.helper.system", SystemHelper=_Dummy)
|
||||||
_stub_module("app.helper.image", ImageHelper=_Dummy)
|
_stub_module("app.helper.image", ImageHelper=_Dummy)
|
||||||
_stub_module("app.scheduler", Scheduler=_Dummy)
|
_stub_module("app.scheduler", Scheduler=_Dummy)
|
||||||
@@ -86,6 +92,12 @@ _stub_module("version", APP_VERSION="test", FRONTEND_VERSION="frontend-test")
|
|||||||
|
|
||||||
from app.api.endpoints import system as system_endpoint
|
from app.api.endpoints import system as system_endpoint
|
||||||
|
|
||||||
|
for _module_name, _module in _ORIGINAL_STUBBED_MODULES.items():
|
||||||
|
if _module is None:
|
||||||
|
sys.modules.pop(_module_name, None)
|
||||||
|
else:
|
||||||
|
sys.modules[_module_name] = _module
|
||||||
|
|
||||||
|
|
||||||
class NettestSecurityTest(unittest.TestCase):
|
class NettestSecurityTest(unittest.TestCase):
|
||||||
def test_get_env_setting_reports_rust_available_and_enabled_separately(self):
|
def test_get_env_setting_reports_rust_available_and_enabled_separately(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user