refactor: 收口 V3 分层架构与插件兼容边界

This commit is contained in:
jxxghp
2026-08-18 13:22:02 +08:00
parent cca99bd421
commit 8472bcff43
274 changed files with 10730 additions and 6130 deletions
+72 -71
View File
@@ -16,7 +16,6 @@ from typing import Any, AsyncIterator, Callable, Optional, Union
from fastapi import Depends, File, Form, HTTPException, Request, UploadFile, status
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.agent import AgentChatDisplaySaveRequest as _SchemaAgentChatDisplaySaveRequest
from app.schemas.agent import AgentChatSessionDetail as _SchemaAgentChatSessionDetail
@@ -45,12 +44,14 @@ from app.chain.message import MessageChain
from app.command import Command
from app.runtime.config import global_vars, settings
from app.runtime.events import Event, EventManager
from app.db import get_async_db
from app.db.oper.agentchat import AgentChatOper
from app.db.models import User
from app.db.models.agentchat import AgentChat
from app.db.oper.user import UserOper
from app.api.deps import get_current_active_user
from app.api.principal import ApiPrincipal
from app.api.deps import get_agent_chat_service, get_current_active_user
from app.application.messaging.chat import (
AgentChatRecord,
AgentChatService,
get_configured_agent_chat_service,
)
from app.application.security.user import get_configured_user_id_lookup
from app.application.messaging.agent import attach_web_agent_edit_queue, detach_web_agent_edit_queue
from app.application.messaging.agent import agent_interaction_manager
from app.application.messaging.agent import (
@@ -187,7 +188,7 @@ class _WebAgentEventPublisher:
self._pending_signal.clear()
def _ensure_superuser(user: User) -> None:
def _ensure_superuser(user: ApiPrincipal) -> None:
"""校验当前用户是否为超级管理员。"""
if not getattr(user, "is_superuser", False):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
@@ -199,7 +200,7 @@ def _ensure_superuser(user: User) -> None:
response_model=_SchemaResponse[_SchemaAgentMcpServerListData],
)
async def list_agent_mcp_servers(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
查询 Agent 外部 MCP 服务器配置。
@@ -224,7 +225,7 @@ async def list_agent_mcp_servers(
)
async def save_agent_mcp_servers(
request: _SchemaAgentMcpServersSaveRequest,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
保存 Agent 外部 MCP 服务器配置。
@@ -244,7 +245,7 @@ async def save_agent_mcp_servers(
)
async def test_agent_mcp_server(
request: _SchemaAgentMcpServerTestRequest,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
测试 Agent 外部 MCP 服务器连接并读取工具列表。
@@ -431,7 +432,7 @@ class _WebAgentMoviePilotAgentMixin:
if not self.user_id:
return False
try:
user = await UserOper().async_get_by_id(int(self.user_id))
user = get_configured_user_id_lookup()(int(self.user_id))
except (TypeError, ValueError):
return False
except Exception as e:
@@ -486,7 +487,7 @@ def _get_web_agent_type() -> type:
return _WEB_AGENT_TYPE
def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
def _build_web_agent_session_id(user: ApiPrincipal, session_id: Optional[str]) -> str:
"""
构建前端 Agent 会话 ID。
@@ -498,8 +499,8 @@ def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
if seed.startswith(WEB_AGENT_SESSION_PREFIX):
return seed
try:
existing_chat = AgentChatOper().get(session_id=seed)
if existing_chat and _can_access_agent_chat(existing_chat, user):
existing_chat = get_configured_agent_chat_service().get_sync(seed)
if existing_chat and AgentChatService.can_access(existing_chat, user):
return seed
except Exception as e:
logger.debug(f"读取WebAgent历史会话失败: {e}")
@@ -508,7 +509,7 @@ def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
return f"{WEB_AGENT_SESSION_PREFIX}{digest[:32]}"
def _can_access_agent_chat(chat: AgentChat, user: User) -> bool:
def _can_access_agent_chat(chat: Any, user: ApiPrincipal) -> bool:
"""
判断当前登录用户是否可以访问指定 Agent 会话。
@@ -524,15 +525,14 @@ def _can_access_agent_chat(chat: AgentChat, user: User) -> bool:
async def _get_accessible_agent_chat(
oper: AgentChatOper, session_id: str, user: User
) -> Optional[AgentChat]:
service: AgentChatService,
session_id: str,
user: ApiPrincipal,
) -> Optional[AgentChatRecord]:
"""
读取当前用户可访问的 Agent 会话。
"""
chat = await oper.async_get(session_id=session_id)
if not chat or not _can_access_agent_chat(chat, user):
return None
return chat
return await service.get_accessible(session_id, user)
def _append_web_agent_text_segment(assistant_message: dict, content: str) -> None:
@@ -631,7 +631,7 @@ def _apply_web_agent_display_event(event: dict, assistant_message: dict) -> None
def _save_web_agent_display_snapshot(
*,
session_id: str,
current_user: User,
current_user: ApiPrincipal,
messages: list[dict],
client_session_id: Optional[str] = None,
) -> None:
@@ -639,9 +639,9 @@ def _save_web_agent_display_snapshot(
保存 WebAgent 当前展示消息快照。
"""
try:
oper = AgentChatOper()
existing_chat = oper.get(session_id=session_id)
AgentChatOper().save_display_messages(
service = get_configured_agent_chat_service()
existing_chat = service.get_sync(session_id)
service.save_display_sync(
session_id=session_id,
user_id=(existing_chat.user_id if existing_chat else str(current_user.id)),
username=(existing_chat.username if existing_chat else current_user.name),
@@ -716,7 +716,7 @@ def _sanitize_web_agent_upload_name(
return safe_name
def _get_web_agent_upload_dir(user: User, session_id: Optional[str]) -> Path:
def _get_web_agent_upload_dir(user: ApiPrincipal, session_id: Optional[str]) -> Path:
"""
计算当前 Web Agent 会话的临时附件目录。
@@ -1425,7 +1425,7 @@ def _get_web_agent_unknown_command_message(text: str) -> Optional[str]:
return f"命令不存在:{command}"
def _ensure_web_agent_command_allowed(current_user: User) -> Optional[str]:
def _ensure_web_agent_command_allowed(current_user: ApiPrincipal) -> Optional[str]:
"""
校验当前 Web 用户是否可以执行传统斜杠命令。
@@ -1440,7 +1440,7 @@ def _ensure_web_agent_command_allowed(current_user: User) -> Optional[str]:
async def _collect_web_agent_traditional_events(
*,
text: str,
current_user: User,
current_user: ApiPrincipal,
original_message_id: Optional[Union[str, int]] = None,
original_chat_id: Optional[Union[str, int]] = None,
) -> list[dict]:
@@ -1637,7 +1637,7 @@ async def download_web_agent_file(file_id: str) -> FileResponse:
async def upload_web_agent_file(
file: UploadFile = File(...),
session_id: Optional[str] = Form(None),
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
上传 Web 智能助手对话附件。
@@ -1680,7 +1680,7 @@ async def upload_web_agent_file(
)
async def web_agent_callback(
payload: _SchemaAgentWebChoiceRequest,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
接收 Web 智能助手选择卡片回调。
@@ -1717,7 +1717,7 @@ async def web_agent_callback(
response_model=_SchemaResponse[list[_SchemaAgentWebCommandInfo]],
)
async def list_web_agent_commands(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> _SchemaResponse:
"""
获取当前 Web 智能助手可补全的斜杠命令。
@@ -1737,8 +1737,8 @@ async def list_web_agent_commands(
response_model=_SchemaResponse[list[_SchemaAgentChatSessionSummary]],
)
async def list_agent_chat_sessions(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
page: Optional[int] = 1,
count: Optional[int] = 30,
) -> _SchemaResponse:
@@ -1746,23 +1746,17 @@ async def list_agent_chat_sessions(
获取当前用户可访问的 Agent 历史会话列表。
:param current_user: 当前登录用户
:param db: 异步数据库会话
:param service: Agent 会话应用服务
:param page: 页码
:param count: 每页数量
:return: 会话摘要列表
"""
user_id = None if current_user.is_superuser else str(current_user.id)
username = None if current_user.is_superuser else current_user.name
chats = await AgentChatOper(db).async_list_by_page(
chats = await service.list(
current_user,
page=page,
count=count,
user_id=user_id,
username=username,
)
return _SchemaResponse(
success=True,
data=[AgentChatOper.to_summary(chat) for chat in chats],
)
return _SchemaResponse(success=True, data=chats)
@router.get(
@@ -1772,24 +1766,27 @@ async def list_agent_chat_sessions(
)
async def get_agent_chat_session(
session_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
) -> _SchemaResponse:
"""
获取一条 Agent 历史会话详情。
:param session_id: Agent 会话 ID
:param current_user: 当前登录用户
:param db: 异步数据库会话
:param service: Agent 会话应用服务
:return: 会话详情
"""
oper = AgentChatOper(db)
chat = await _get_accessible_agent_chat(oper, session_id, current_user)
chat = await _get_accessible_agent_chat(service, session_id, current_user)
server_session_id = session_id
if not chat:
server_session_id = _build_web_agent_session_id(current_user, session_id)
if server_session_id != session_id:
chat = await _get_accessible_agent_chat(oper, server_session_id, current_user)
chat = await _get_accessible_agent_chat(
service,
server_session_id,
current_user,
)
if not chat:
manager = get_running_agent_manager()
if manager and manager.is_session_busy(server_session_id):
@@ -1803,7 +1800,7 @@ async def get_agent_chat_session(
},
)
return _SchemaResponse(success=False, message="会话不存在或无权访问")
data = AgentChatOper.to_detail(chat)
data = service.to_detail(chat).model_dump()
manager = get_running_agent_manager()
data["is_processing"] = bool(
manager and manager.is_session_busy(chat.session_id)
@@ -1819,8 +1816,8 @@ async def get_agent_chat_session(
async def save_agent_chat_display(
session_id: str,
payload: _SchemaAgentChatDisplaySaveRequest,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
) -> _SchemaResponse:
"""
保存前端聚合后的 Agent 展示消息。
@@ -1828,12 +1825,15 @@ async def save_agent_chat_display(
:param session_id: Agent 会话 ID
:param payload: 展示消息保存请求
:param current_user: 当前登录用户
:param db: 异步数据库会话
:param service: Agent 会话应用服务
:return: 保存后的会话摘要
"""
oper = AgentChatOper(db)
existing_chat = await oper.async_get(session_id=session_id)
if existing_chat and not _can_access_agent_chat(existing_chat, current_user):
existing_chat = await service.get_accessible(session_id, current_user)
if existing_chat is None:
unrestricted_chat = await service.get(session_id)
else:
unrestricted_chat = existing_chat
if unrestricted_chat and existing_chat is None:
return _SchemaResponse(success=False, message="会话不存在或无权访问")
messages = [
@@ -1847,10 +1847,10 @@ async def save_agent_chat_display(
messages=messages,
client_session_id=existing_chat.client_session_id if existing_chat else session_id,
)
chat = await oper.async_get(session_id=session_id)
chat = await service.get_accessible(session_id, current_user)
if not chat:
return _SchemaResponse(success=False, message="会话保存失败")
return _SchemaResponse(success=True, data=AgentChatOper.to_summary(chat))
return _SchemaResponse(success=True, data=service.to_summary(chat))
@router.delete(
@@ -1860,22 +1860,21 @@ async def save_agent_chat_display(
)
async def delete_agent_chat_session(
session_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
) -> _SchemaResponse:
"""
删除一条 Agent 历史会话。
:param session_id: Agent 会话 ID
:param current_user: 当前登录用户
:param db: 异步数据库会话
:param service: Agent 会话应用服务
:return: 删除结果
"""
oper = AgentChatOper(db)
chat = await _get_accessible_agent_chat(oper, session_id, current_user)
chat = await _get_accessible_agent_chat(service, session_id, current_user)
if not chat:
return _SchemaResponse(success=False, message="会话不存在或无权访问")
deleted = await oper.async_delete(session_id=session_id)
deleted = await service.delete(session_id, current_user)
return _SchemaResponse(success=deleted, message="删除成功" if deleted else "删除失败")
@@ -1886,23 +1885,25 @@ async def delete_agent_chat_session(
)
async def stop_web_agent_session_task(
session_id: str,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
current_user: ApiPrincipal = Depends(get_current_active_user),
service: AgentChatService = Depends(get_agent_chat_service),
) -> _SchemaResponse:
"""
停止当前 Web 智能助手会话正在执行的任务。
:param session_id: Agent 会话 ID
:param current_user: 当前登录用户
:param db: 异步数据库会话
:param service: Agent 会话应用服务
:return: 停止结果
"""
server_session_id = _build_web_agent_session_id(current_user, session_id)
chat = await _get_accessible_agent_chat(
AgentChatOper(db), server_session_id, current_user
service,
server_session_id,
current_user,
)
if not chat and server_session_id != session_id:
chat = await _get_accessible_agent_chat(AgentChatOper(db), session_id, current_user)
chat = await _get_accessible_agent_chat(service, session_id, current_user)
if chat and not _can_access_agent_chat(chat, current_user):
return _SchemaResponse(success=False, message="会话不存在或无权访问")
@@ -1930,7 +1931,7 @@ async def stop_web_agent_session_task(
async def web_agent_stream(
payload: _SchemaAgentWebChatRequest,
request: Request,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> StreamingResponse:
"""
Web 智能助手流式对话。
+1 -1
View File
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.anilist import AniListChain
from app.domain.context import MediaInfo
from app.application.security.access import verify_token
from app.adapters.web.security.access import verify_token
router = ResponseAPIRouter()
+1 -1
View File
@@ -23,7 +23,7 @@ from app.api.openai_utils import (
)
from app.agent.runtime_loader import get_running_agent_manager
from app.runtime.config import settings
from app.application.security.access import anthropic_api_key_header
from app.adapters.web.security.access import anthropic_api_key_header
ANTHROPIC_ERROR_RESPONSES = {
400: {"model": _SchemaAnthropicErrorResponse, "description": "请求格式错误"},
+14 -12
View File
@@ -1,15 +1,14 @@
from typing import Any
from fastapi import HTTPException
from fastapi import Depends, HTTPException
from pydantic import BaseModel
from app.schemas.token import Token as _SchemaToken
from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.security.auth import build_token_response, consume_plugin_auth_ticket
from app.runtime.extensions.plugin_manager import PluginManager
from app.db.models.passkey import PassKey
from app.db.models.user import User
from app.application.security.auth import AuthService, consume_plugin_auth_ticket
from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.api.deps import get_auth_service
router = ResponseAPIRouter()
@@ -22,13 +21,13 @@ class AuthExchangeRequest(BaseModel):
ticket: str
def _system_auth_providers() -> list[dict[str, Any]]:
def _system_auth_providers(service: AuthService) -> list[dict[str, Any]]:
"""
获取系统内建的匿名登录方式摘要。
:return: 系统认证提供方列表
"""
has_passkey = bool(PassKey.list(db=None))
has_passkey = service.has_passkey()
return [
{
"id": "system:passkey",
@@ -46,13 +45,13 @@ def _system_auth_providers() -> list[dict[str, Any]]:
summary="查询登录认证提供方",
response_model=list[_SchemaAuthProviderInfo],
)
def auth_providers() -> list[dict[str, Any]]:
def auth_providers(service: AuthService = Depends(get_auth_service)) -> list[dict[str, Any]]:
"""
查询系统和插件提供的登录认证入口。
:return: 认证提供方摘要列表
"""
providers = _system_auth_providers()
providers = _system_auth_providers(service)
providers.extend(PluginManager().get_plugin_auth_providers())
return [provider for provider in providers if provider.get("enabled", True)]
@@ -63,7 +62,10 @@ def auth_providers() -> list[dict[str, Any]]:
response_model=_SchemaToken,
openapi_extra={RAW_RESPONSE_OPENAPI_KEY: True},
)
def auth_exchange(body: AuthExchangeRequest) -> _SchemaToken:
def auth_exchange(
body: AuthExchangeRequest,
service: AuthService = Depends(get_auth_service),
) -> _SchemaToken:
"""
将插件认证成功后生成的一次性票据兑换为系统 Token。
@@ -74,8 +76,8 @@ def auth_exchange(body: AuthExchangeRequest) -> _SchemaToken:
if not ticket_data:
raise HTTPException(status_code=401, detail="认证票据无效或已过期")
user = User.get(db=None, rid=ticket_data.get("user_id"))
user = service.get_user_by_id(ticket_data.get("user_id"))
if not user or not user.is_active:
raise HTTPException(status_code=403, detail="用户不存在或已禁用")
return build_token_response(user)
return service.build_token_response(user)
+1 -1
View File
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.bangumi import BangumiChain
from app.domain.context import MediaInfo
from app.application.security.access import verify_token
from app.adapters.web.security.access import verify_token
router = ResponseAPIRouter()
+10 -48
View File
@@ -2,7 +2,6 @@ from pathlib import Path
from typing import Any, List, Optional, Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryInfo
from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo
@@ -17,53 +16,17 @@ from app.api.response import ResponseAPIRouter
from app.chain.dashboard import DashboardChain
from app.chain.storage import StorageChain
from app.runtime.config import settings
from app.application.security.access import verify_apitoken
from app.db import get_db
from app.db.models.transferhistory import TransferHistory
from app.api.deps import get_current_active_superuser
from app.adapters.web.security.access import verify_apitoken
from app.api.deps import get_current_active_superuser, get_dashboard_query_service
from app.application.dashboard import DashboardQueryService
from app.schemas.types import StorageAction
from app.application.directory import DirectoryHelper
from app.scheduler import Scheduler
from app.application.scheduling import Scheduler
from app.adapters.system.host import SystemUtils
router = ResponseAPIRouter()
def _build_statistic(db: Session, name: Optional[str] = None) -> _SchemaStatistic:
"""
构建媒体数量统计信息。
"""
media_statistics: Optional[List[_SchemaStatistic]] = (
DashboardChain().media_statistic(name)
)
if media_statistics:
# 汇总各媒体库统计信息
ret_statistic = _SchemaStatistic()
has_episode_count = False
for media_statistic in media_statistics:
ret_statistic.movie_count += media_statistic.movie_count or 0
ret_statistic.tv_count += media_statistic.tv_count or 0
ret_statistic.music_count += media_statistic.music_count or 0
ret_statistic.user_count += media_statistic.user_count or 0
if media_statistic.episode_count is not None:
ret_statistic.episode_count += media_statistic.episode_count or 0
has_episode_count = True
if not has_episode_count:
# 所有媒体服务都未提供剧集统计时,返回 None 供前端展示“未获取”。
ret_statistic.episode_count = None
else:
ret_statistic = _SchemaStatistic()
movie_count_month, tv_count_month, episode_count_month, music_count_month = (
TransferHistory.monthly_media_statistics(db)
)
ret_statistic.movie_count_month = movie_count_month
ret_statistic.tv_count_month = tv_count_month
ret_statistic.episode_count_month = episode_count_month
ret_statistic.music_count_month = music_count_month
return ret_statistic
def _build_storage() -> _SchemaStorage:
"""
构建本地存储空间信息。
@@ -114,13 +77,13 @@ def _build_downloader(name: Optional[str] = None) -> _SchemaDownloaderInfo:
@router.get("/statistic", summary="媒体数量统计", response_model=_SchemaStatistic)
def statistic(
name: Optional[str] = None,
db: Session = Depends(get_db),
service: DashboardQueryService = Depends(get_dashboard_query_service),
_: Any = Depends(get_current_active_superuser),
) -> Any:
"""
查询媒体数量统计信息
"""
return _build_statistic(db, name)
return service.statistic(name)
@router.get(
@@ -128,12 +91,12 @@ def statistic(
)
def statistic2(
_: Annotated[str, Depends(verify_apitoken)],
db: Session = Depends(get_db),
service: DashboardQueryService = Depends(get_dashboard_query_service),
) -> Any:
"""
查询媒体数量统计信息 API_TOKEN认证(?token=xxx
"""
return _build_statistic(db)
return service.statistic()
@router.get("/storage", summary="本地存储空间", response_model=_SchemaStorage)
@@ -249,14 +212,13 @@ async def schedule_progress2(
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
async def transfer(
days: Optional[int] = 7,
db: Session = Depends(get_db),
service: DashboardQueryService = Depends(get_dashboard_query_service),
_: Any = Depends(get_current_active_superuser),
) -> Any:
"""
查询文件整理统计信息
"""
transfer_stat = await TransferHistory.async_statistic(db, days)
return [stat[1] for stat in transfer_stat]
return await service.transfer(days)
@router.get("/cpu", summary="获取当前CPU使用率", response_model=float)
+1 -1
View File
@@ -10,7 +10,7 @@ from app.chain.bangumi import BangumiChain
from app.chain.douban import DoubanChain
from app.chain.tmdb import TmdbChain
from app.runtime.events import eventmanager
from app.application.security.access import verify_token
from app.adapters.web.security.access import verify_token
from app.schemas.event import DiscoverSourceEventData
from app.schemas.types import ChainEventType, MediaType
+1 -1
View File
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.douban import DoubanChain
from app.domain.context import MediaInfo
from app.application.security.access import verify_token
from app.adapters.web.security.access import verify_token
from app.schemas.types import MediaType
router = ResponseAPIRouter()
+25 -12
View File
@@ -20,11 +20,14 @@ from app.chain.media import MediaChain
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.application.security.access import verify_token
from app.db.models.user import User
from app.db.oper.site import SiteOper
from app.db.oper.systemconfig import SystemConfigOper
from app.api.deps import get_current_active_user
from app.adapters.web.security.access import verify_token
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.application.site.query import (
SiteQueryService,
get_configured_site_query_service,
)
from app.api.deps import get_current_active_user, get_site_sync_query_service
from app.application.directory import DirectoryHelper
from app.schemas.types import (
MUSIC_ENTITY_RECORDING,
@@ -39,7 +42,10 @@ from app.application.security.url import SecurityUtils
router = ResponseAPIRouter()
def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
def _prepare_subtitle_download(
subtitle: SubtitleInfo,
query: SiteQueryService | None = None,
) -> tuple[bool, str]:
"""
校验字幕下载签名,并用服务端站点配置覆盖请求凭据。
"""
@@ -53,7 +59,8 @@ def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
if not clean_url:
return False, "字幕下载链接签名无效"
site = SiteOper().get(subtitle.site)
site_query = query or get_configured_site_query_service()
site = site_query.get_sync(subtitle.site)
if not site:
return False, "字幕站点信息不存在"
@@ -84,7 +91,7 @@ def download(
torrent_in: _SchemaTorrentInfo,
downloader: Annotated[str | None, Body()] = None,
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
添加下载任务(含媒体信息)
@@ -130,7 +137,7 @@ def add(
downloader: Annotated[str | None, Body()] = None,
# 保存路径, 支持<storage>:<path>, 如rclone:/MP, smb:/server/share/Movies等
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
添加下载任务(不含媒体信息)
@@ -213,14 +220,20 @@ def download_subtitle(
media_source: Annotated[MediaSource, Body()],
media_id: Annotated[str, Body()],
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
query: SiteQueryService = Depends(get_site_sync_query_service),
) -> Any:
"""
下载字幕资源。
"""
subtitle_info = SubtitleInfo()
subtitle_info.from_dict(subtitle_in.model_dump())
valid, message = _prepare_subtitle_download(subtitle_info)
# 直接调用 endpoint 的旧测试/插件入口不会经过 FastAPI 依赖解析;此时让
# 应用查询端口自行提供服务,仍保留真实请求中的注入对象。
if not hasattr(query, "get_sync"):
valid, message = _prepare_subtitle_download(subtitle_info)
else:
valid, message = _prepare_subtitle_download(subtitle_info, query)
if not valid:
return _SchemaResponse(success=False, message=message)
@@ -273,7 +286,7 @@ async def clients(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
"""
查询可用下载器
"""
downloaders: List[dict] = SystemConfigOper().get(SystemConfigKey.Downloaders)
downloaders: List[dict] = get_configured_system_config().get(SystemConfigKey.Downloaders)
if downloaders:
return [
{"name": d.get("name"), "type": d.get("type")}
+22 -73
View File
@@ -3,8 +3,6 @@ import time
from typing import List, Any, Optional
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.schemas.common import BatchProgressKeyData as _SchemaBatchProgressKeyData
from app.schemas.common import ProgressKeyData as _SchemaProgressKeyData
@@ -22,23 +20,20 @@ from app.agent.prompt.transfer_redo import (
build_manual_redo_prompt,
)
from app.runtime.config import settings, global_vars
from app.application.security.access import verify_token
from app.db import get_async_db, get_db
from app.db.models import User
from app.db.models.downloadhistory import DownloadHistory
from app.db.models.transferhistory import TransferHistory
from app.adapters.web.security.access import verify_token
from app.api.deps import (
get_current_active_manage_user,
get_current_active_superuser,
get_download_history_mutation_command,
get_history_query_service,
get_transfer_history_mutation_command,
)
from app.runtime.progress import ProgressHelper
from app.application.history import (
DownloadHistoryMutationCommand,
HistoryQueryService,
TransferHistoryMutationCommand,
)
from app.foundation.text import cut as jieba_cut
from app.runtime.log import logger
router = ResponseAPIRouter()
@@ -155,13 +150,13 @@ def _start_batch_ai_redo_task(
async def download_history(
page: Optional[int] = 1,
count: Optional[int] = 30,
db: AsyncSession = Depends(get_async_db),
query: HistoryQueryService = Depends(get_history_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
按下载时间倒序查询下载历史记录
"""
return await DownloadHistory.async_list_by_page(db, page, count)
return await query.list_download(page=page, count=count)
@router.delete(
@@ -183,14 +178,6 @@ def delete_download_history(
return _SchemaResponse(success=result.success, message=result.message)
def _glob_to_like(pattern: str) -> str:
"""
将 glob 通配符模式转换为 SQL LIKE 模式(使用 \\ 作为转义字符)
"""
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
return result.replace("*", "%").replace("?", "_")
@router.get(
"/transfer",
summary="查询整理记录",
@@ -201,50 +188,19 @@ async def transfer_history(
page: Optional[int] = 1,
count: Optional[int] = 30,
status: Optional[bool] = None,
db: AsyncSession = Depends(get_async_db),
query: HistoryQueryService = Depends(get_history_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*
"""
if title == "失败":
title = None
status = False
elif title == "成功":
title = None
status = True
if title:
if "*" in title or "?" in title:
like_pattern = _glob_to_like(title)
total = await TransferHistory.async_count_by_title(
db, title=like_pattern, status=status, wildcard=True
)
result = await TransferHistory.async_list_by_title(
db, title=like_pattern, page=page, count=count, status=status, wildcard=True
)
else:
words = jieba_cut(title, HMM=False)
like_pattern = "%".join(words)
total = await TransferHistory.async_count_by_title(
db, title=like_pattern, status=status
)
result = await TransferHistory.async_list_by_title(
db, title=like_pattern, page=page, count=count, status=status
)
else:
result = await TransferHistory.async_list_by_page(
db, page=page, count=count, status=status
)
total = await TransferHistory.async_count(db, status=status)
return _SchemaResponse(
success=True,
data={
"list": [item.to_dict() for item in result],
"total": total,
},
result = await query.list_transfer(
title=title,
page=page,
count=count,
status=status,
)
return _SchemaResponse(success=True, data=result)
@router.delete("/transfer", summary="删除整理记录", response_model=_SchemaResponse[None])
@@ -255,7 +211,7 @@ def delete_transfer_history(
command: TransferHistoryMutationCommand = Depends(
get_transfer_history_mutation_command
),
_: User = Depends(get_current_active_manage_user),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
删除整理记录。
@@ -273,10 +229,10 @@ def delete_transfer_history(
summary="智能助手重新整理",
response_model=_SchemaResponse[_SchemaProgressKeyData],
)
def ai_redo_transfer_history(
async def ai_redo_transfer_history(
history_id: int,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
query: HistoryQueryService = Depends(get_history_query_service),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
手动触发单条历史记录的 AI 重新整理,并返回进度键。
@@ -284,7 +240,7 @@ def ai_redo_transfer_history(
if not settings.AI_AGENT_ENABLE:
return _SchemaResponse(success=False, message="MoviePilot智能助手未启用")
history = TransferHistory.get(db, history_id)
history = await query.get_transfer(history_id)
if not history:
return _SchemaResponse(success=False, message="整理记录不存在")
@@ -304,10 +260,10 @@ def ai_redo_transfer_history(
summary="智能助手批量重新整理",
response_model=_SchemaResponse[_SchemaBatchProgressKeyData],
)
def batch_ai_redo_transfer_history(
async def batch_ai_redo_transfer_history(
payload: _SchemaBatchTransferHistoryRedoRequest,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
query: HistoryQueryService = Depends(get_history_query_service),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
手动触发多条历史记录的 AI 批量重新整理,并返回进度键。
@@ -319,14 +275,7 @@ def batch_ai_redo_transfer_history(
if not history_ids:
return _SchemaResponse(success=False, message="未提供有效的整理记录")
histories = []
missing_ids = []
for history_id in history_ids:
history = TransferHistory.get(db, history_id)
if not history:
missing_ids.append(history_id)
continue
histories.append(history)
histories, missing_ids = await query.get_transfers(history_ids)
if missing_ids:
return _SchemaResponse(
@@ -358,7 +307,7 @@ def empty_transfer_history(
command: TransferHistoryMutationCommand = Depends(
get_transfer_history_mutation_command
),
_: User = Depends(get_current_active_superuser),
_: object = Depends(get_current_active_superuser),
) -> Any:
"""
清空整理记录
+1 -2
View File
@@ -6,7 +6,6 @@ from fastapi.responses import HTMLResponse
from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.db.models import User
from app.api.deps import get_current_active_superuser_async
router = ResponseAPIRouter()
@@ -29,7 +28,7 @@ def _get_llm_provider_manager_type() -> type:
async def manage_provider(
request: Request,
payload: _SchemaManageRequest,
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
):
"""
LLM 提供商统一管理入口:前端上送 target/action/params 原样透传,
+6 -5
View File
@@ -11,9 +11,10 @@ from app.schemas.token import Token as _SchemaToken
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.chain.user import MfaRequired, UserChain
from app.application.security import access as security
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.application.security.token import create_access_token
from app.runtime.config import settings
from app.db.oper.systemconfig import SystemConfigOper
from app.application.configuration import get_configured_system_config
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
from app.application.image import WallpaperHelper
from app.schemas.types import SystemConfigKey
@@ -67,17 +68,17 @@ def login_access_token(
level = SitesHelper().auth_level
# 是否显示配置向导
show_wizard = (
not SystemConfigOper().get(SystemConfigKey.SetupWizardState)
not get_configured_system_config().get(SystemConfigKey.SetupWizardState)
and not settings.ADVANCED_MODE
)
access_token = security.create_access_token(
access_token = create_access_token(
userid=user_or_message.id,
username=user_or_message.name,
super_user=user_or_message.is_superuser,
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
level=level,
)
security.set_or_refresh_resource_token_cookie(
set_or_refresh_resource_token_cookie(
request,
response,
_SchemaTokenPayload(
+1 -1
View File
@@ -13,7 +13,7 @@ from app.schemas.mcp import ToolCallRequest as _SchemaToolCallRequest
from app.schemas.response import Response as _SchemaResponse
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.agent.tools.manager import moviepilot_tool_manager
from app.application.security.access import verify_apikey
from app.adapters.web.security.access import verify_apikey
from app.runtime.log import logger
# 导入版本号
+3 -4
View File
@@ -25,8 +25,7 @@ from app.domain.context import Context, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.application.security.access import verify_token, verify_apitoken
from app.db.models import User
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.deps import get_current_active_user, get_current_active_superuser
from app.schemas.category import CategoryConfig
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
@@ -440,7 +439,7 @@ def scrape(
summary="获取分类策略配置",
response_model=_SchemaResponse[_SchemaCategoryConfig],
)
def get_category_config(_: User = Depends(get_current_active_user)):
def get_category_config(_: object = Depends(get_current_active_user)):
"""
获取分类策略配置
"""
@@ -452,7 +451,7 @@ def get_category_config(_: User = Depends(get_current_active_user)):
"/category/config", summary="保存分类策略配置", response_model=_SchemaResponse[None]
)
def save_category_config(
config: CategoryConfig, _: User = Depends(get_current_active_superuser)
config: CategoryConfig, _: object = Depends(get_current_active_superuser)
):
"""
保存分类策略配置
+9 -12
View File
@@ -1,7 +1,6 @@
from typing import Any, List, Optional
from fastapi import Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo
from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo
@@ -19,12 +18,10 @@ from app.chain.download import DownloadChain
from app.chain.mediaserver import MediaServerChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.application.security.access import verify_token
from app.db import get_async_db
from app.db.oper.mediaserver import MediaServerOper
from app.db.models import MediaServerItem
from app.db.oper.systemconfig import SystemConfigOper
from app.application.mediaserver import MediaServerHelper
from app.adapters.web.security.access import verify_token
from app.application.configuration import get_configured_system_config
from app.application.mediaserver import MediaServerHelper, MediaServerQueryService
from app.api.deps import get_mediaserver_query_service
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.types import MediaSource, MediaType, SystemConfigKey
from app.schemas.media import build_media_key, resolve_media_identity
@@ -90,7 +87,7 @@ async def exists_local(
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
season: Optional[int] = None,
db: AsyncSession = Depends(get_async_db),
service: MediaServerQueryService = Depends(get_mediaserver_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
@@ -107,7 +104,7 @@ async def exists_local(
# 返回对象
ret_info = {}
# 本地数据库是否存在
exist: MediaServerItem = await MediaServerOper(db).async_exists(
item_id = await service.find_item_id(
title=meta.name if meta else None,
year=year,
mtype=mtype,
@@ -115,8 +112,8 @@ async def exists_local(
media_id=media_id,
season=season,
)
if exist:
ret_info = {"id": exist.item_id}
if item_id:
ret_info = {"id": item_id}
return _SchemaResponse(success=True, data={"item": ret_info})
@@ -251,7 +248,7 @@ async def clients(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
"""
查询可用媒体服务器
"""
mediaservers: List[dict] = SystemConfigOper().get(SystemConfigKey.MediaServers)
mediaservers: List[dict] = get_configured_system_config().get(SystemConfigKey.MediaServers)
if mediaservers:
return [
{"name": d.get("name"), "type": d.get("type")}
+14 -24
View File
@@ -5,7 +5,6 @@ import time
from typing import Protocol, Union, Any, List, Optional
from fastapi import BackgroundTasks, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import PlainTextResponse
from app.schemas.message import MessageClearBefore as _SchemaMessageClearBefore
@@ -20,13 +19,12 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import ResponseAPIRouter
from app.chain.message import MessageChain
from app.runtime.config import settings, global_vars
from app.application.security.access import verify_token, verify_apitoken
from app.db import get_async_db
from app.db.models import User
from app.db.oper.message import MessageOper
from app.db.oper.systemconfig import SystemConfigOper
from app.api.deps import get_current_active_superuser
from app.runtime.extensions.service_registry import ServiceConfigHelper
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.deps import get_current_active_superuser, get_message_query_service
from app.application.messaging.message import MessageQueryService
from app.runtime.extensions.service_config import ServiceConfigHelper
from app.runtime.log import logger
from app.adapters.external.wechat_crypt import WXBizMsgCrypt
from app.schemas.types import NotificationChannel, SystemConfigKey
@@ -83,7 +81,7 @@ def _get_notification_clear_before() -> _SchemaMessageClearBefore:
"""
读取通知中心清理时间配置。
"""
value = SystemConfigOper().get(SystemConfigKey.NotificationClearBefore)
value = get_configured_system_config().get(SystemConfigKey.NotificationClearBefore)
if isinstance(value, dict):
return _SchemaMessageClearBefore(
all=_normalize_notification_clear_timestamp(value.get("all")),
@@ -156,7 +154,7 @@ async def user_message(
async def web_message(
request: Request,
text: Optional[str] = None,
current_user: User = Depends(get_current_active_superuser),
current_user: ApiPrincipal = Depends(get_current_active_superuser),
):
"""
WEB消息响应
@@ -194,28 +192,20 @@ async def web_message(
@router.get("/web", summary="获取WEB消息", response_model=List[_SchemaWebMessageItem])
async def get_web_message(
_: _SchemaTokenPayload = Depends(verify_token),
db: AsyncSession = Depends(get_async_db),
service: MessageQueryService = Depends(get_message_query_service),
page: Optional[int] = 1,
count: Optional[int] = 20,
):
"""
获取WEB消息列表
"""
ret_messages = []
messages = await MessageOper(db).async_list_by_page(page=page, count=count)
for message in messages:
try:
ret_messages.append(message.to_dict())
except Exception as e:
logger.error(f"获取WEB消息列表失败: {str(e)}")
continue
return ret_messages
return await service.list_web(page=page, count=count)
@router.get("/notification", summary="获取通知消息", response_model=List[_SchemaMessageHistoryItem])
async def get_notification_message(
_: _SchemaTokenPayload = Depends(verify_token),
db: AsyncSession = Depends(get_async_db),
service: MessageQueryService = Depends(get_message_query_service),
page: Optional[int] = 1,
count: Optional[int] = 20,
):
@@ -223,14 +213,14 @@ async def get_notification_message(
获取系统发送的通知消息列表。
"""
clear_before = _get_notification_clear_before()
messages = await MessageOper(db).async_list_sent_by_page(
messages = await service.list_notifications(
page=page,
count=count,
all_clear_before=_format_notification_clear_time(clear_before.all),
system_clear_before=_format_notification_clear_time(clear_before.system),
media_clear_before=_format_notification_clear_time(clear_before.media),
)
return [_SchemaMessageHistoryItem(**message.to_dict()) for message in messages]
return [_SchemaMessageHistoryItem(**message) for message in messages]
@router.delete(
@@ -248,7 +238,7 @@ async def clear_notification_message(
clear_before = _get_notification_clear_before()
value = clear_before.model_dump()
value[scope.value] = int(time.time() * 1000)
await SystemConfigOper().async_set(SystemConfigKey.NotificationClearBefore, value)
await get_configured_system_config().async_set(SystemConfigKey.NotificationClearBefore, value)
return _SchemaResponse(success=True, data={"clear_before": value})
+75 -77
View File
@@ -4,12 +4,9 @@ MFA (Multi-Factor Authentication) API 端点
"""
import json
from datetime import timedelta
from typing import Any, Annotated, Optional
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
from fastapi import Depends, HTTPException, Body, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.mcp import BaseModel as _SchemaBaseModel
from app.schemas.mcp import JsonData as _SchemaJsonData
@@ -21,13 +18,24 @@ from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import Token as _SchemaToken
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.security import access as security
from app.runtime.config import settings
from app.db import get_async_db
from app.db.models.passkey import PassKey
from app.db.models.user import User
from app.db.oper.systemconfig import SystemConfigOper
from app.api.deps import get_current_active_user, get_current_active_user_async
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.application.security.token import verify_password
from app.application.security.auth import get_configured_auth_service
from app.application.security.user import UserService
from app.application.security.user import (
get_configured_user_id_lookup,
get_configured_user_name_lookup,
)
from app.application.security.passkeys import (
PasskeyService,
)
from app.api.principal import ApiPrincipal
from app.api.deps import (
get_current_active_user,
get_current_active_user_async,
get_user_service,
get_passkey_service,
)
from app.application.security.passkey import (
PassKeyHelper,
PassKeyRegistrationOriginMismatchError,
@@ -35,7 +43,6 @@ from app.application.security.passkey import (
PasskeyChallengeStore,
)
from app.runtime.log import logger
from app.schemas.types import SystemConfigKey
from app.application.security.otp import OtpUtils
router = ResponseAPIRouter()
@@ -43,7 +50,7 @@ router = ResponseAPIRouter()
# ==================== 辅助函数 ====================
def _build_credential_list(passkeys: list[PassKey]) -> list[dict[str, Any]]:
def _build_credential_list(passkeys: list[Any]) -> list[dict[str, Any]]:
"""
构建凭证列表
@@ -75,7 +82,10 @@ def _extract_and_standardize_credential_id(credential: dict) -> str:
def _verify_passkey_and_update(
credential: dict, challenge: str, passkey: PassKey
credential: dict,
challenge: str,
passkey: Any,
service: PasskeyService,
) -> tuple[bool, int]:
"""
验证 PassKey 并更新使用时间和签名计数
@@ -93,7 +103,7 @@ def _verify_passkey_and_update(
)
if success:
passkey.update_last_used(db=None, sign_count=new_sign_count)
service.update_last_used(passkey, new_sign_count)
return success, new_sign_count
@@ -129,11 +139,14 @@ class PassKeyDeleteRequest(_SchemaBaseModel):
summary="判断用户是否开启二次验证",
response_model=_SchemaResponse[_SchemaMfaStatusData],
)
async def mfa_status(username: str, db: AsyncSession = Depends(get_async_db)) -> Any:
async def mfa_status(
username: str,
service: UserService = Depends(get_user_service),
) -> Any:
"""
检查指定用户是否启用了二次验证
"""
user: User = await User.async_get_by_name(db, username)
user = await service.get_by_name(username)
if not user:
return _SchemaResponse(success=False, message="用户不存在")
@@ -152,7 +165,7 @@ async def mfa_status(username: str, db: AsyncSession = Depends(get_async_db)) ->
response_model=_SchemaResponse[_SchemaOtpGenerateData],
)
def otp_generate(
current_user: Annotated[User, Depends(get_current_active_user)],
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
) -> Any:
"""生成 OTP 密钥及对应的 URI"""
secret, uri = OtpUtils.generate_secret_key(current_user.name)
@@ -162,14 +175,16 @@ def otp_generate(
@router.post("/otp/verify", summary="绑定并验证 OTP", response_model=_SchemaResponse[None])
async def otp_verify(
data: OtpVerifyRequest,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
service: UserService = Depends(get_user_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""验证用户输入的 OTP 码,验证通过后正式开启 OTP 验证"""
if not OtpUtils.is_legal(data.uri, data.otpPassword):
return _SchemaResponse(success=False, message="验证码错误")
await current_user.async_update_otp_by_name(
db, current_user.name, True, OtpUtils.get_secret(data.uri)
await service.update_otp(
current_user.name,
True,
OtpUtils.get_secret(data.uri),
)
return _SchemaResponse(success=True)
@@ -181,14 +196,14 @@ async def otp_verify(
)
async def otp_disable(
data: OtpDisableRequest,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
service: UserService = Depends(get_user_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""关闭当前用户的 OTP 验证功能"""
# 验证密码
if not security.verify_password(data.password, str(current_user.hashed_password)):
if not verify_password(data.password, str(current_user.hashed_password)):
return _SchemaResponse(success=False, message="密码错误")
await current_user.async_update_otp_by_name(db, current_user.name, False, "")
await service.update_otp(current_user.name, False, "")
return _SchemaResponse(success=True)
@@ -228,12 +243,13 @@ class PassKeyAuthenticationFinish(_SchemaBaseModel):
response_model=_SchemaResponse[_SchemaPasskeyStartData],
)
def passkey_register_start(
current_user: Annotated[User, Depends(get_current_active_user)],
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""开始注册 PassKey - 生成注册选项"""
try:
# 获取用户已有的PassKey
existing_passkeys = PassKey.get_by_user_id(db=None, user_id=current_user.id)
existing_passkeys = service.list_by_user_id(current_user.id)
existing_credentials = (
_build_credential_list(existing_passkeys) if existing_passkeys else None
)
@@ -272,7 +288,8 @@ def passkey_register_start(
)
def passkey_register_finish(
passkey_req: PassKeyRegistrationFinish,
current_user: Annotated[User, Depends(get_current_active_user)],
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""完成注册 PassKey - 验证并保存凭证"""
try:
@@ -303,16 +320,15 @@ def passkey_register_finish(
transports = ",".join(passkey_req.credential["response"]["transports"])
# 保存到数据库
passkey = PassKey(
user_id=current_user.id,
credential_id=credential_id,
public_key=public_key,
sign_count=sign_count,
name=passkey_req.name or "通行密钥",
aaguid=aaguid,
transports=transports,
)
passkey.create()
service.create({
"user_id": current_user.id,
"credential_id": credential_id,
"public_key": public_key,
"sign_count": sign_count,
"name": passkey_req.name or "通行密钥",
"aaguid": aaguid,
"transports": transports,
})
logger.info(f"用户 {current_user.name} 成功注册PassKey: {passkey_req.name}")
@@ -339,6 +355,7 @@ def passkey_register_finish(
)
def passkey_authenticate_start(
passkey_req: PassKeyAuthenticationStart = Body(...),
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""开始 PassKey 认证 - 生成认证选项"""
try:
@@ -347,9 +364,9 @@ def passkey_authenticate_start(
# 如果指定了用户名,只允许该用户的PassKey
if passkey_req.username:
user = User.get_by_name(db=None, name=passkey_req.username)
user = get_configured_user_name_lookup()(passkey_req.username)
existing_passkeys = (
PassKey.get_by_user_id(db=None, user_id=user.id) if user else None
service.list_by_user_id(user.id) if user else None
)
if not user or not existing_passkeys:
@@ -387,7 +404,10 @@ def passkey_authenticate_start(
openapi_extra={RAW_RESPONSE_OPENAPI_KEY: True},
)
def passkey_authenticate_finish(
request: Request, response: Response, passkey_req: PassKeyAuthenticationFinish
request: Request,
response: Response,
passkey_req: PassKeyAuthenticationFinish,
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""完成 PassKey 认证 - 验证凭证并返回 token"""
try:
@@ -408,8 +428,8 @@ def passkey_authenticate_finish(
raise HTTPException(status_code=401, detail="认证失败")
# 查找PassKey并获取用户
passkey = PassKey.get_by_credential_id(db=None, credential_id=credential_id)
user = User.get_by_id(db=None, user_id=passkey.user_id) if passkey else None
passkey = service.get_by_credential_id(credential_id)
user = get_configured_user_id_lookup()(passkey.user_id) if passkey else None
if not passkey or not user or not user.is_active:
raise HTTPException(status_code=401, detail="认证失败")
if challenge_state.user_id is not None and challenge_state.user_id != user.id:
@@ -420,6 +440,7 @@ def passkey_authenticate_finish(
credential=passkey_req.credential,
challenge=challenge_state.challenge,
passkey=passkey,
service=service,
)
if not success:
@@ -428,42 +449,19 @@ def passkey_authenticate_finish(
logger.info(f"用户 {user.name} 通过PassKey认证成功")
# 生成token
level = SitesHelper().auth_level
show_wizard = (
not SystemConfigOper().get(SystemConfigKey.SetupWizardState)
and not settings.ADVANCED_MODE
)
access_token = security.create_access_token(
userid=user.id,
username=user.name,
super_user=user.is_superuser,
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
level=level,
)
security.set_or_refresh_resource_token_cookie(
token = get_configured_auth_service().build_token_response(user)
set_or_refresh_resource_token_cookie(
request,
response,
_SchemaTokenPayload(
sub=user.id,
username=user.name,
super_user=user.is_superuser,
level=level,
level=token.level,
purpose="authentication",
),
)
return _SchemaToken(
access_token=access_token,
token_type="bearer",
super_user=user.is_superuser,
user_id=user.id,
user_name=user.name,
avatar=user.avatar,
level=level,
permissions=user.permissions or {},
wizard=show_wizard,
)
return token
except HTTPException:
raise
except Exception as e:
@@ -477,11 +475,12 @@ def passkey_authenticate_finish(
response_model=_SchemaResponse[list[_SchemaPasskeyInfo]],
)
def passkey_list(
current_user: Annotated[User, Depends(get_current_active_user)],
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""获取当前用户的所有 PassKey"""
try:
passkeys = PassKey.get_by_user_id(db=None, user_id=current_user.id)
passkeys = service.list_by_user_id(current_user.id)
key_list = (
[
@@ -514,19 +513,18 @@ def passkey_list(
)
async def passkey_delete(
data: PassKeyDeleteRequest,
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
service: PasskeyService = Depends(get_passkey_service),
) -> Any:
"""删除指定的 PassKey"""
try:
# 验证密码
if not security.verify_password(
if not verify_password(
data.password, str(current_user.hashed_password)
):
return _SchemaResponse(success=False, message="密码错误")
success = PassKey.delete_by_id(
db=None, passkey_id=data.passkey_id, user_id=current_user.id
)
success = service.delete_by_id(data.passkey_id, current_user.id)
if success:
logger.info(f"用户 {current_user.name} 删除了PassKey: {data.passkey_id}")
+4 -5
View File
@@ -14,8 +14,7 @@ from app.chain.media import MediaChain
from app.chain.recommend import RecommendChain
from app.schemas.types import MediaSource, MediaType
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
from app.application.security.access import verify_token
from app.db.models.user import User
from app.adapters.web.security.access import verify_token
from app.api.deps import get_current_active_superuser_async
from app.chain.listenbrainz import (
LISTENBRAINZ_CHART_RANGES,
@@ -114,7 +113,7 @@ async def recognize_music(
response_model=_SchemaResponse[_SchemaMusicRecognitionCacheData],
)
async def music_recognition_cache(
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""查询可管理的 MusicBrainz 识别缓存。"""
cache_items = MusicBrainzChain().cache_items()
@@ -137,7 +136,7 @@ async def music_recognition_cache(
)
async def delete_music_recognition_cache(
cache_key: str,
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""按缓存键删除单条 MusicBrainz 识别缓存。"""
deleted_item = MusicBrainzChain().delete_cache(cache_key)
@@ -150,7 +149,7 @@ async def delete_music_recognition_cache(
"/cache", summary="清空音乐识别缓存", response_model=_SchemaResponse[None]
)
async def clear_music_recognition_cache(
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""清空全部 MusicBrainz 识别缓存。"""
MusicBrainzChain().clear_cache()
+1 -2
View File
@@ -6,7 +6,6 @@ from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.notification import NotificationChain
from app.db.models import User
from app.api.deps import get_current_active_superuser
router = ResponseAPIRouter()
@@ -19,7 +18,7 @@ router = ResponseAPIRouter()
)
def manage_channel(
request: _SchemaManageRequest,
_: User = Depends(get_current_active_superuser),
_: object = Depends(get_current_active_superuser),
):
"""
通知渠道统一管理入口
+1 -1
View File
@@ -32,7 +32,7 @@ from app.agent.runtime_loader import (
)
from app.agent.contracts import ReplyMode
from app.runtime.config import settings
from app.application.security.access import openai_bearer_scheme
from app.adapters.web.security.access import openai_bearer_scheme
from app.schemas.types import NotificationChannel
OPENAI_ERROR_RESPONSES = {
+52 -43
View File
@@ -35,14 +35,18 @@ from app.application.commands import init_commands
from app.application.scheduling import remove_plugin_job, update_plugin_job
from app.runtime.cache import async_fresh
from app.runtime.config import settings
from app.runtime.extensions.plugin_manager import PluginManager
from app.application.security.access import (
from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.runtime.extensions.plugin.contracts import (
PluginDashboardError,
PluginNotFoundError,
)
from app.adapters.web.security.access import (
resource_token_cookie,
verify_resource_token,
verify_token,
)
from app.db.models import User
from app.db.oper.systemconfig import SystemConfigOper
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.deps import (
get_current_active_superuser,
get_current_active_superuser_async,
@@ -240,7 +244,7 @@ async def _get_plugin_history_detail(
@router.get("/", summary="所有插件", response_model=List[_SchemaPlugin])
async def all_plugins(
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
state: Optional[str] = "all",
force: bool = False,
) -> List[_SchemaPlugin]:
@@ -298,17 +302,17 @@ async def all_plugins(
@router.get("/installed", summary="已安装插件", response_model=List[str])
async def installed(_: User = Depends(get_current_active_superuser_async)) -> Any:
async def installed(_: ApiPrincipal = Depends(get_current_active_superuser_async)) -> Any:
"""
查询用户已安装插件清单
"""
return SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
return get_configured_system_config().get(SystemConfigKey.UserInstalledPlugins) or []
@router.get("/history/{plugin_id}", summary="获取插件更新说明", response_model=_SchemaPlugin)
async def plugin_history(
plugin_id: str,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
force: bool = True,
) -> _SchemaPlugin:
"""
@@ -330,7 +334,7 @@ async def plugin_history(
)
async def plugin_releases(
plugin_id: str,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
repo_url: Optional[str] = "",
force: bool = False,
) -> dict:
@@ -405,7 +409,7 @@ async def statistic(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
)
async def plugin_ratings(
plugin_ids: Optional[str] = None,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> Dict[str, _SchemaPluginRating]:
"""
批量查询插件平均分、评分人数和当前安装实例评分。
@@ -425,7 +429,7 @@ async def plugin_ratings(
)
async def plugin_rating(
plugin_id: str,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaPluginRating:
"""
查询单个插件平均分、评分人数和当前安装实例评分。
@@ -442,12 +446,12 @@ async def plugin_rating(
async def rate_plugin(
plugin_id: str,
payload: _SchemaPluginRatingRequest,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""
为已安装插件新增或更新当前安装实例评分。
"""
installed_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
installed_plugins = get_configured_system_config().get(SystemConfigKey.UserInstalledPlugins) or []
if plugin_id not in installed_plugins:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -467,7 +471,7 @@ async def rate_plugin(
"/reload/{plugin_id}", summary="重新加载插件", response_model=_SchemaResponse[None]
)
def reload_plugin(
plugin_id: str, _: User = Depends(get_current_active_superuser)
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> Any:
"""
重新加载插件
@@ -485,7 +489,7 @@ async def install(
repo_url: Optional[str] = "",
release_version: Optional[str] = None,
force: Optional[bool] = False,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> Any:
"""
安装插件
@@ -495,7 +499,7 @@ async def install(
async def save_installed_plugins(plugin_ids: List[str]) -> object:
"""保存安装用例确认后的插件列表。"""
return await SystemConfigOper().async_set(
return await get_configured_system_config().async_set(
SystemConfigKey.UserInstalledPlugins,
plugin_ids,
)
@@ -523,7 +527,7 @@ async def install(
return await run_in_threadpool(register_plugin, target_id)
command = PluginInstallCommand(
installed_plugins_reader=lambda: SystemConfigOper().get(
installed_plugins_reader=lambda: get_configured_system_config().get(
SystemConfigKey.UserInstalledPlugins
) or [],
installed_plugins_writer=save_installed_plugins,
@@ -585,7 +589,7 @@ def plugin_sidebar_nav(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
response_model=_SchemaJsonObject,
)
def plugin_form(
plugin_id: str, _: User = Depends(get_current_active_superuser)
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> dict:
"""
根据插件ID获取插件配置表单或Vue组件URL
@@ -621,7 +625,7 @@ def plugin_form(
response_model=_SchemaJsonObject,
)
def plugin_page(
plugin_id: str, _: User = Depends(get_current_active_superuser)
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> dict:
"""
根据插件ID获取插件数据页面
@@ -649,7 +653,7 @@ def plugin_page(
response_model=List[_SchemaPluginDashboardMetaItem],
)
def plugin_dashboard_meta(
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
) -> List[dict]:
"""
获取所有插件仪表板元信息
@@ -662,19 +666,24 @@ def plugin_dashboard_by_key(
plugin_id: str,
key: str,
user_agent: Annotated[str | None, Header()] = None,
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
) -> Optional[_SchemaPluginDashboard]:
"""
根据插件ID获取插件仪表板
"""
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
try:
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
except PluginNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except PluginDashboardError as error:
raise HTTPException(status_code=500, detail=str(error)) from error
@router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置")
def plugin_dashboard(
plugin_id: str,
user_agent: Annotated[str | None, Header()] = None,
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
) -> Optional[_SchemaPluginDashboard]:
"""
根据插件ID获取插件仪表板
@@ -687,7 +696,7 @@ def plugin_dashboard(
)
def reset_plugin(
plugin_id: str,
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
command: PluginConfigCommand = Depends(get_plugin_config_command),
) -> Any:
"""
@@ -798,13 +807,13 @@ async def plugin_static_file(
response_model=_SchemaPluginFoldersData,
)
async def get_plugin_folders(
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> dict:
"""
获取插件文件夹分组配置
"""
try:
result = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
result = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
return result
except Exception as e:
logger.error(f"[文件夹API] 获取文件夹配置失败: {str(e)}")
@@ -813,13 +822,13 @@ async def get_plugin_folders(
@router.post("/folders", summary="保存插件文件夹配置", response_model=_SchemaResponse[None])
async def save_plugin_folders(
folders: dict, _: User = Depends(get_current_active_superuser_async)
folders: dict, _: ApiPrincipal = Depends(get_current_active_superuser_async)
) -> Any:
"""
保存插件文件夹分组配置
"""
try:
SystemConfigOper().set(SystemConfigKey.PluginFolders, folders)
get_configured_system_config().set(SystemConfigKey.PluginFolders, folders)
return _SchemaResponse(success=True)
except Exception as e:
logger.error(f"[文件夹API] 保存文件夹配置失败: {str(e)}")
@@ -830,15 +839,15 @@ async def save_plugin_folders(
"/folders/{folder_name}", summary="创建插件文件夹", response_model=_SchemaResponse[None]
)
async def create_plugin_folder(
folder_name: str, _: User = Depends(get_current_active_superuser_async)
folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
) -> Any:
"""
创建新的插件文件夹
"""
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
if folder_name not in folders:
folders[folder_name] = []
SystemConfigOper().set(SystemConfigKey.PluginFolders, folders)
get_configured_system_config().set(SystemConfigKey.PluginFolders, folders)
return _SchemaResponse(
success=True, message=f"文件夹 '{folder_name}' 创建成功"
)
@@ -850,15 +859,15 @@ async def create_plugin_folder(
"/folders/{folder_name}", summary="删除插件文件夹", response_model=_SchemaResponse[None]
)
async def delete_plugin_folder(
folder_name: str, _: User = Depends(get_current_active_superuser_async)
folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
) -> Any:
"""
删除插件文件夹
"""
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
if folder_name in folders:
del folders[folder_name]
await SystemConfigOper().async_set(SystemConfigKey.PluginFolders, folders)
await get_configured_system_config().async_set(SystemConfigKey.PluginFolders, folders)
return _SchemaResponse(
success=True, message=f"文件夹 '{folder_name}' 删除成功"
)
@@ -874,14 +883,14 @@ async def delete_plugin_folder(
async def update_folder_plugins(
folder_name: str,
plugin_ids: List[str],
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> Any:
"""
更新指定文件夹中的插件列表
"""
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
folders[folder_name] = plugin_ids
await SystemConfigOper().async_set(SystemConfigKey.PluginFolders, folders)
await get_configured_system_config().async_set(SystemConfigKey.PluginFolders, folders)
return _SchemaResponse(
success=True, message=f"文件夹 '{folder_name}' 中的插件已更新"
)
@@ -891,7 +900,7 @@ async def update_folder_plugins(
"/clone/{plugin_id}", summary="创建插件分身", response_model=_SchemaResponse[None]
)
def clone_plugin(
plugin_id: str, clone_data: dict, _: User = Depends(get_current_active_superuser)
plugin_id: str, clone_data: dict, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> Any:
"""
创建插件分身
@@ -925,7 +934,7 @@ def clone_plugin(
response_model=_SchemaJsonObject,
)
async def plugin_config(
plugin_id: str, _: User = Depends(get_current_active_superuser_async)
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
) -> dict:
"""
根据插件ID获取插件配置信息
@@ -937,7 +946,7 @@ async def plugin_config(
def set_plugin_config(
plugin_id: str,
conf: dict,
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
command: PluginConfigCommand = Depends(get_plugin_config_command),
) -> Any:
"""
@@ -949,12 +958,12 @@ def set_plugin_config(
@router.delete("/{plugin_id}", summary="卸载插件", response_model=_SchemaResponse[None])
def uninstall_plugin(
plugin_id: str, _: User = Depends(get_current_active_superuser)
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> Any:
"""
卸载插件
"""
config_oper = SystemConfigOper()
config_oper = get_configured_system_config()
# 删除已安装信息
install_plugins = config_oper.get(SystemConfigKey.UserInstalledPlugins) or []
for plugin in install_plugins:
@@ -995,7 +1004,7 @@ def _add_clone_to_plugin_folder(original_plugin_id: str, clone_plugin_id: str):
:param clone_plugin_id: 分身插件ID
"""
try:
config_oper = SystemConfigOper()
config_oper = get_configured_system_config()
# 获取插件文件夹配置
folders = config_oper.get(SystemConfigKey.PluginFolders) or {}
+1 -1
View File
@@ -9,7 +9,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.recommend import RecommendChain
from app.runtime.events import eventmanager
from app.application.security.access import verify_token
from app.adapters.web.security.access import verify_token
from app.schemas.exception import TMDbException
from app.schemas.event import RecommendSourceEventData
from app.schemas.types import ChainEventType
+1 -1
View File
@@ -16,7 +16,7 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import Context as _SchemaContext
from app.api.response import ResponseAPIRouter
from app.chain.search import SearchChain
from app.application.security.access import verify_resource_token, verify_token
from app.adapters.web.security.access import verify_resource_token, verify_token
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.schemas.types import MediaSource, MediaType
+80 -93
View File
@@ -1,8 +1,6 @@
from typing import List, Any, Dict, Optional
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from starlette.background import BackgroundTasks
from app.schemas.common import JsonObject as _SchemaJsonObject
@@ -19,32 +17,28 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import Site as _SchemaSite
from app.api.response import ResponseAPIRouter
from app.application.site.mutation import SiteMutationCommand
from app.application.site.query import SiteQueryService
from app.api.endpoints.plugin import register_plugin_api
from app.chain.site import SiteChain
from app.chain.torrents import TorrentsChain
from app.command import Command
from app.runtime.events import eventmanager
from app.runtime.extensions.plugin_manager import PluginManager
from app.application.security.access import verify_token
from app.db import get_db, get_async_db
from app.db.models import User
from app.db.models.site import Site
from app.db.models.siteicon import SiteIcon
from app.db.models.sitestatistic import SiteStatistic
from app.db.models.siteuserdata import SiteUserData
from app.db.oper.site import SiteOper
from app.db.oper.systemconfig import SystemConfigOper
from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.adapters.web.security.access import verify_token
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.deps import (
get_current_active_manage_user,
get_current_active_manage_user_async,
get_current_active_superuser,
get_current_active_superuser_async,
get_site_mutation_command,
get_site_query_service,
get_site_sync_query_service,
)
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
from app.runtime.log import logger
from app.scheduler import Scheduler
from app.schemas.types import SystemConfigKey, EventType, MediaType
from app.application.scheduling import Scheduler
from app.schemas.types import SystemConfigKey, MediaType
from app.domain import site as site_rules
router = ResponseAPIRouter()
@@ -88,13 +82,13 @@ def _indexer_supports_media_type(indexer: dict, media_type: MediaType) -> bool:
@router.get("/", summary="所有站点", response_model=List[_SchemaSite])
async def read_sites(
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> List[dict]:
"""
获取站点列表
"""
return await Site.async_list_order_by_pri(db)
return await query.list_ordered()
@router.get(
@@ -104,14 +98,14 @@ async def read_sites(
)
async def read_sites_by_media_type(
media_type: str,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
) -> List[Site]:
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> List[_SchemaSite]:
"""
获取支持指定媒体类型的已配置启用站点
:param media_type: Agent 媒体类型名称或中文媒体类型
:param db: 异步数据库会话
:param query: 站点查询服务
:return: 按优先级排序的可搜索站点
"""
target_media_type = MediaType.from_agent(media_type)
@@ -134,7 +128,7 @@ async def read_sites_by_media_type(
if domain:
supported_domains.add(domain)
sites = await Site.async_list_order_by_pri(db)
sites = await query.list_ordered()
return [
site
for site in sites
@@ -148,7 +142,7 @@ async def add_site(
*,
site_in: _SchemaSite,
command: SiteMutationCommand = Depends(get_site_mutation_command),
_: User = Depends(get_current_active_manage_user_async),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
新增站点
@@ -162,7 +156,7 @@ async def update_site(
*,
site_in: _SchemaSite,
command: SiteMutationCommand = Depends(get_site_mutation_command),
_: User = Depends(get_current_active_manage_user_async),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
更新站点信息
@@ -174,7 +168,7 @@ async def update_site(
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=_SchemaResponse[None])
async def cookie_cloud_sync(
background_tasks: BackgroundTasks,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> Any:
"""
运行CookieCloud同步站点信息
@@ -184,20 +178,20 @@ async def cookie_cloud_sync(
@router.get("/reset", summary="重置站点", response_model=_SchemaResponse[None])
def reset(
db: AsyncSession = Depends(get_db), _: User = Depends(get_current_active_superuser)
async def reset(
command: SiteMutationCommand = Depends(get_site_mutation_command),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> Any:
"""
清空所有站点数据并重新同步CookieCloud站点信息
"""
Site.reset(db)
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
SystemConfigOper().set(SystemConfigKey.RssSites, [])
result = await command.reset()
get_configured_system_config().set(SystemConfigKey.IndexerSites, [])
get_configured_system_config().set(SystemConfigKey.RssSites, [])
# 启动定时服务
Scheduler().start("cookiecloud", manual=True)
# 插件站点删除
eventmanager.send_event(EventType.SiteDeleted, {"site_id": "*"})
return _SchemaResponse(success=True, message="站点已重置!")
return _SchemaResponse(success=result.success, message="站点已重置!")
@router.post(
@@ -206,7 +200,7 @@ def reset(
async def update_sites_priority(
priorities: List[dict],
command: SiteMutationCommand = Depends(get_site_mutation_command),
_: User = Depends(get_current_active_manage_user_async),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
批量更新站点优先级
@@ -220,7 +214,7 @@ def _update_site_cookie(
username: str,
password: str,
code: Optional[str],
db: Session,
query: SiteQueryService,
) -> _SchemaResponse:
"""
执行站点 Cookie UA 更新
@@ -229,10 +223,10 @@ def _update_site_cookie(
:param username: 站点登录用户名
:param password: 站点登录密码
:param code: 二步验证码或密钥
:param db: 数据库会话
:param query: 站点查询服务
:return: 更新结果
"""
site_info = Site.get(db, site_id)
site_info = query.get_sync(site_id)
if not site_info:
raise HTTPException(
status_code=404,
@@ -255,8 +249,8 @@ def _update_site_cookie(
def update_cookie_by_body(
site_id: int,
site_cookie_update: _SchemaSiteCookieUpdate,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
query: SiteQueryService = Depends(get_site_sync_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
使用请求体中的用户密码更新站点Cookie
@@ -266,7 +260,7 @@ def update_cookie_by_body(
username=site_cookie_update.username,
password=site_cookie_update.password,
code=site_cookie_update.code,
db=db,
query=query,
)
@@ -278,8 +272,8 @@ def update_cookie(
username: str,
password: str,
code: Optional[str] = None,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
query: SiteQueryService = Depends(get_site_sync_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
使用用户密码更新站点Cookie
@@ -289,7 +283,7 @@ def update_cookie(
username=username,
password=password,
code=code,
db=db,
query=query,
)
@@ -300,13 +294,13 @@ def update_cookie(
)
def refresh_userdata(
site_id: int,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
query: SiteQueryService = Depends(get_site_sync_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
刷新站点用户数据
"""
site = Site.get(db, site_id)
site = query.get_sync(site_id)
if not site:
raise HTTPException(
status_code=404,
@@ -327,16 +321,13 @@ def refresh_userdata(
response_model=List[_SchemaSiteUserData],
)
async def read_userdata_latest(
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
查询所有站点最新用户数据
"""
user_datas = await SiteUserData.async_get_latest(db)
if not user_datas:
return []
return [user_data.to_dict() for user_data in user_datas]
return await query.userdata_latest()
@router.get(
@@ -347,36 +338,34 @@ async def read_userdata_latest(
async def read_userdata(
site_id: int,
workdate: Optional[str] = None,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
查询站点用户数据
"""
site = await Site.async_get(db, site_id)
site = await query.get(site_id)
if not site:
raise HTTPException(
status_code=404,
detail=f"站点 {site_id} 不存在",
)
user_datas = await SiteUserData.async_get_by_domain(
db, domain=site.domain, workdate=workdate
)
user_datas = await query.userdata(site.domain, workdate)
if not user_datas:
return _SchemaResponse(success=False, data=[])
return _SchemaResponse(success=True, data=[data.to_dict() for data in user_datas])
return _SchemaResponse(success=True, data=user_datas)
@router.get("/test/{site_id}", summary="连接测试", response_model=_SchemaResponse[None])
def test_site(
site_id: int,
db: Session = Depends(get_db),
query: SiteQueryService = Depends(get_site_sync_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
测试站点是否可用
"""
site = Site.get(db, site_id)
site = query.get_sync(site_id)
if not site:
raise HTTPException(
status_code=404,
@@ -393,24 +382,22 @@ def test_site(
)
async def site_icon(
site_id: int,
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
获取站点图标base64或者url
"""
site = await Site.async_get(db, site_id)
site = await query.get(site_id)
if not site:
raise HTTPException(
status_code=404,
detail=f"站点 {site_id} 不存在",
)
icon = await SiteIcon.async_get_by_domain(db, site.domain)
icon = await query.icon(site.domain)
if not icon:
return _SchemaResponse(success=False, message="站点图标不存在!")
return _SchemaResponse(
success=True, data={"icon": icon.base64 if icon.base64 else icon.url}
)
return _SchemaResponse(success=True, data=icon.model_dump())
@router.get(
@@ -418,13 +405,13 @@ async def site_icon(
)
async def site_category(
site_id: int,
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
获取站点分类
"""
site = await Site.async_get(db, site_id)
site = await query.get(site_id)
if not site:
raise HTTPException(
status_code=404,
@@ -456,13 +443,13 @@ async def site_resource(
mtype: Optional[str] = None,
cat: Optional[str] = None,
page: Optional[int] = 0,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
浏览站点资源
"""
site = await Site.async_get(db, site_id)
site = await query.get(site_id)
if not site:
raise HTTPException(
status_code=404,
@@ -483,14 +470,14 @@ async def site_resource(
@router.get("/domain/{site_url}", summary="站点详情", response_model=_SchemaSite)
async def read_site_by_domain(
site_url: str,
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
通过域名获取站点信息
"""
domain = site_rules.extract_domain(site_url)
site = await Site.async_get_by_domain(db, domain)
site = await query.get_by_domain(domain)
if not site:
raise HTTPException(
status_code=404,
@@ -506,45 +493,42 @@ async def read_site_by_domain(
)
async def read_statistic_by_domain(
site_url: str,
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
通过域名获取站点统计信息
"""
domain = site_rules.extract_domain(site_url)
sitestatistic = await SiteStatistic.async_get_by_domain(db, domain)
if sitestatistic:
return sitestatistic
return _SchemaSiteStatistic(domain=domain)
return await query.statistic(domain)
@router.get(
"/statistic", summary="所有站点统计信息", response_model=List[_SchemaSiteStatistic]
)
async def read_statistics(
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""
获取所有站点统计信息
"""
return await SiteStatistic.async_list(db)
return await query.statistics()
@router.get("/rss", summary="所有订阅站点", response_model=List[_SchemaSite])
async def read_rss_sites(
db: AsyncSession = Depends(get_async_db),
query: SiteQueryService = Depends(get_site_query_service),
_: _SchemaTokenPayload = Depends(verify_token),
) -> List[dict]:
"""
获取站点列表
"""
# 选中的rss站点
selected_sites = SystemConfigOper().get(SystemConfigKey.RssSites) or []
selected_sites = get_configured_system_config().get(SystemConfigKey.RssSites) or []
# 所有站点
all_site = await Site.async_list_order_by_pri(db)
all_site = await query.list_ordered()
if not selected_sites:
return all_site
@@ -563,7 +547,7 @@ async def read_auth_sites(_: _SchemaTokenPayload = Depends(verify_token)) -> dic
@router.post("/auth", summary="用户站点认证", response_model=_SchemaResponse[None])
def auth_site(
auth_info: _SchemaSiteAuth, _: User = Depends(get_current_active_superuser)
auth_info: _SchemaSiteAuth, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> Any:
"""
用户站点认证
@@ -571,7 +555,7 @@ def auth_site(
if not auth_info or not auth_info.site or not auth_info.params:
return _SchemaResponse(success=False, message="请输入认证站点和认证参数")
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
SystemConfigOper().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
# 认证成功后,重新初始化插件
PluginManager().init_config()
Scheduler().init_plugin_jobs()
@@ -585,12 +569,15 @@ def auth_site(
summary="获取站点域名到名称的映射",
response_model=_SchemaResponse[_SchemaSiteMappingData],
)
async def site_mapping(_: User = Depends(get_current_active_superuser_async)):
async def site_mapping(
query: SiteQueryService = Depends(get_site_sync_query_service),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
):
"""
获取站点域名到名称的映射关系
"""
try:
sites = await SiteOper().async_list()
sites = query.list_sync()
mapping = {}
for site in sites:
mapping[site.domain] = site.name
@@ -604,7 +591,7 @@ async def site_mapping(_: User = Depends(get_current_active_superuser_async)):
summary="获取支持的站点列表",
response_model=_SchemaJsonObject,
)
async def support_sites(_: User = Depends(get_current_active_superuser_async)):
async def support_sites(_: ApiPrincipal = Depends(get_current_active_superuser_async)):
"""
获取支持的站点列表
"""
@@ -614,13 +601,13 @@ async def support_sites(_: User = Depends(get_current_active_superuser_async)):
@router.get("/{site_id}", summary="站点详情", response_model=_SchemaSite)
async def read_site(
site_id: int,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: SiteQueryService = Depends(get_site_query_service),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
通过ID获取站点信息
"""
site = await Site.async_get(db, site_id)
site = await query.get(site_id)
if not site:
raise HTTPException(
status_code=404,
@@ -633,7 +620,7 @@ async def read_site(
async def delete_site(
site_id: int,
command: SiteMutationCommand = Depends(get_site_mutation_command),
_: User = Depends(get_current_active_manage_user_async),
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
) -> Any:
"""
删除站点
+8 -8
View File
@@ -15,7 +15,7 @@ from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.runtime.config import settings
from app.db.models import User
from app.api.principal import ApiPrincipal
from app.api.deps import (
get_current_active_manage_user,
get_current_active_superuser,
@@ -31,7 +31,7 @@ router = ResponseAPIRouter()
"/manage", summary="网盘存储统一管理", response_model=_SchemaResponse[Dict[str, Any]]
)
def manage(
request: _SchemaManageRequest, _: User = Depends(get_current_active_superuser)
request: _SchemaManageRequest, _: ApiPrincipal = Depends(get_current_active_superuser)
) -> Any:
"""
网盘存储统一管理入口
@@ -56,7 +56,7 @@ def list_files(
fileitem: _SchemaFileItem,
sort: Optional[str] = "updated_at",
keyword: Optional[str] = None,
_: User = Depends(get_current_active_manage_user),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
查询当前目录下所有目录和文件
@@ -82,7 +82,7 @@ def list_files(
def mkdir(
fileitem: _SchemaFileItem,
name: str,
_: User = Depends(get_current_active_manage_user),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
创建目录
@@ -100,7 +100,7 @@ def mkdir(
@router.post("/delete", summary="删除文件或目录", response_model=_SchemaResponse[None])
def delete(
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
) -> Any:
"""
删除文件或目录
@@ -131,7 +131,7 @@ def delete(
},
)
def download(
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
) -> Any:
"""
下载文件或目录
@@ -160,7 +160,7 @@ def download(
},
)
def image(
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
) -> Any:
"""
下载文件或目录
@@ -179,7 +179,7 @@ def rename(
fileitem: _SchemaFileItem,
new_name: str,
recursive: Optional[bool] = False,
_: User = Depends(get_current_active_manage_user),
_: ApiPrincipal = Depends(get_current_active_manage_user),
) -> Any:
"""
重命名文件或目录
+125 -178
View File
@@ -2,8 +2,6 @@ from typing import List, Any, Annotated, Optional
import cn2an
from fastapi import Request, BackgroundTasks, Depends, HTTPException, Header
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.schemas.common import IdData as _SchemaIdData
from app.schemas.response import Response as _SchemaResponse
@@ -19,7 +17,7 @@ from app.runtime.config import settings
from app.domain.context import MediaInfo
from app.runtime.events import eventmanager
from app.domain.metainfo import MetaInfo
from app.application.security.access import verify_token, verify_apitoken
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.application.subscription.delete import (
DeleteSubscribeCommand,
SubscribeDeletionActor,
@@ -31,20 +29,25 @@ from app.application.subscription.search import (
SearchSubscriptionsCommand,
SubscribeSearchActor,
)
from app.db import get_async_db, get_db
from app.db.models.subscribe import Subscribe
from app.db.models.subscribehistory import SubscribeHistory
from app.db.models.user import User
from app.db.oper.systemconfig import SystemConfigOper
from app.api.principal import ApiPrincipal
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.mutation import (
SubscriptionActor,
SubscriptionMutationService,
)
from app.application.configuration import get_configured_system_config
from app.api.deps import (
get_current_active_user,
get_current_active_user_async,
get_delete_subscribe_command,
get_delete_subscriptions_by_identity_command,
get_search_subscriptions_command,
get_subscription_query_service,
get_subscription_mutation_service,
get_subscription_sync_mutation_service,
)
from app.adapters.external.server import MoviePilotServerHelper
from app.scheduler import Scheduler
from app.application.scheduling import Scheduler
from app.schemas.event import SubscribeModifiedEventData
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -82,16 +85,15 @@ def start_subscribe_add(
)
def build_subscribe_event_payload(subscribe: Subscribe) -> dict:
def build_subscribe_event_payload(subscribe: Any) -> dict:
"""
ORM 已加载字段构造订阅事件快照避免异步接口里属性懒加载触发隐式 IO
"""
values = subscribe.__dict__
return {column.name: values.get(column.name) for column in subscribe.__table__.columns}
return subscribe.to_dict()
def can_access_subscribe(
subscribe: Subscribe | SubscribeHistory | None, current_user: User
subscribe: Any, current_user: ApiPrincipal
) -> bool:
"""
判断当前用户是否可访问订阅及其历史记录
@@ -107,33 +109,9 @@ def can_access_subscribe(
return bool(username) and username == current_user.name
async def get_accessible_subscribe(
db: AsyncSession, subscribe_id: int, current_user: User
) -> Subscribe | None:
"""
按订阅 ID 读取当前用户可访问的订阅行
"""
subscribe = await Subscribe.async_get(db, subscribe_id)
if can_access_subscribe(subscribe, current_user):
return subscribe
return None
def get_accessible_subscribe_sync(
db: Session, subscribe_id: int, current_user: User
) -> Subscribe | None:
"""
同步读取当前用户可访问的订阅行
"""
subscribe = Subscribe.get(db, subscribe_id)
if can_access_subscribe(subscribe, current_user):
return subscribe
return None
def select_accessible_subscribe(
subscribes: List[Subscribe], current_user: User
) -> Subscribe | None:
subscribes: List[Any], current_user: ApiPrincipal
) -> Any:
"""
从候选订阅中选择当前用户可访问的第一条记录
"""
@@ -144,7 +122,7 @@ def select_accessible_subscribe(
def matches_subscribe_music_type(
subscribe: Subscribe,
subscribe: Any,
music_type: Optional[str],
) -> bool:
"""匹配订阅音乐实体,并把迁移前未标注类型的历史记录兼容为单曲。"""
@@ -155,54 +133,30 @@ def matches_subscribe_music_type(
or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None)
async def list_subscribes_by_media_identity(
db: AsyncSession,
media_source: MediaSource,
media_id: str,
season: Optional[int] = None,
music_type: Optional[str] = None,
) -> List[Subscribe]:
"""按媒体来源、原生 ID 及音乐实体查询订阅。"""
subscribes = list(await Subscribe.async_list_by_media_identity(
db,
media_source=media_source,
media_id=media_id,
music_type=music_type,
))
unique_subscribes = {
subscribe.id: subscribe
for subscribe in subscribes
if matches_subscribe_music_type(subscribe, music_type)
}
if season is not None:
return [
subscribe for subscribe in unique_subscribes.values()
if subscribe.season == season
]
return list(unique_subscribes.values())
@router.get("/", summary="查询所有订阅", response_model=List[_SchemaSubscribe])
async def read_subscribes(
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询所有订阅
"""
if not current_user.is_superuser:
return await Subscribe.async_list_by_username(db, current_user.name)
return await Subscribe.async_list(db)
return await query.list_public(current_user.name)
return await query.list_public()
@router.get(
"/list", summary="查询所有订阅(API_TOKEN", response_model=List[_SchemaSubscribe]
)
async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
async def list_subscribes(
query: SubscriptionQueryService = Depends(get_subscription_query_service),
_: Annotated[str, Depends(verify_apitoken)] = None,
) -> Any:
"""
查询所有订阅 API_TOKEN认证?token=xxx
"""
return await Subscribe.async_list()
return await query.list_public()
@router.post(
@@ -213,7 +167,7 @@ async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
async def create_subscribe(
*,
subscribe_in: _SchemaSubscribe,
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> _SchemaResponse:
"""
新增订阅
@@ -274,13 +228,17 @@ async def create_subscribe(
async def update_subscribe(
*,
subscribe_in: _SchemaSubscribe,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
更新订阅信息
"""
subscribe = await get_accessible_subscribe(db, subscribe_in.id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = await mutation.get_accessible(subscribe_in.id, actor)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
old_subscribe_dict = subscribe.to_dict()
@@ -326,16 +284,21 @@ async def update_subscribe(
if total_episode_updated and subscribe_in.total_episode != subscribe.total_episode:
subscribe_dict["manual_total_episode"] = 1
# 更新到数据库
await subscribe.async_update(db, subscribe_dict)
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subscribe_in.id)
change = await mutation.update(
subscribe_in.id,
subscribe_dict,
actor,
existing=subscribe,
)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subscribe_in.id,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="update",
).to_dict(),
)
@@ -346,29 +309,29 @@ async def update_subscribe(
async def update_subscribe_status(
subid: int,
state: str,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
更新订阅状态
"""
subscribe = await get_accessible_subscribe(db, subid, current_user)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
valid_states = ["R", "P", "S"]
if state not in valid_states:
return _SchemaResponse(success=False, message="无效的订阅状态")
old_subscribe_dict = subscribe.to_dict()
await subscribe.async_update(db, {"state": state})
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subid)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
change = await mutation.update_status(subid, state, actor)
if not change:
return _SchemaResponse(success=False, message="订阅不存在")
# 发送订阅调整事件
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="status",
).to_dict(),
)
@@ -382,22 +345,22 @@ async def subscribe_media_identity(
season: Optional[int] = None,
title: Optional[str] = None,
music_type: Optional[str] = None,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据媒体来源和原生 ID 查询订阅
"""
subscribes = await list_subscribes_by_media_identity(
db, media_source, media_id, season, music_type
)
subscribes = await query.list_by_media_identity(media_source, media_id, music_type)
if season is not None:
subscribes = [subscribe for subscribe in subscribes if subscribe.season == season]
result = select_accessible_subscribe(subscribes, current_user)
return result if result else Subscribe()
return result if result else _SchemaSubscribe()
@router.get("/refresh", summary="刷新订阅", response_model=_SchemaResponse[None])
def refresh_subscribes(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
刷新所有订阅
@@ -411,44 +374,24 @@ def refresh_subscribes(
@router.get("/reset/{subid}", summary="重置订阅", response_model=_SchemaResponse[None])
async def reset_subscribes(
subid: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
重置订阅
"""
subscribe = await get_accessible_subscribe(db, subid, current_user)
if subscribe:
# 在更新之前获取旧数据
old_subscribe_dict = subscribe.to_dict()
# 更新订阅
await subscribe.async_update(
db,
{
"note": [],
"lack_episode": subscribe.total_episode,
"current_priority": None,
"current_audio_format": None,
"current_bitrate": None,
"current_bit_depth": None,
"current_sample_rate": None,
"episode_priority": {},
# 重置代表放弃手动总集数,后续订阅检查重新按 TMDB 集数更新。
"manual_total_episode": 0,
"state": "R",
},
)
# 重新获取更新后的订阅数据
updated_subscribe = await Subscribe.async_get(db, subid)
# 发送订阅调整事件
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
change = await mutation.reset(subid, actor)
if change:
await eventmanager.async_send_event(
EventType.SubscribeModified,
SubscribeModifiedEventData(
subscribe_id=subid,
old_subscribe_info=old_subscribe_dict,
subscribe_info=updated_subscribe.to_dict()
if updated_subscribe
else {},
old_subscribe_info=change.old,
subscribe_info=change.new,
scene="reset",
).to_dict(),
)
@@ -458,7 +401,7 @@ async def reset_subscribes(
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=_SchemaResponse[None])
def check_subscribes(
current_user: User = Depends(get_current_active_user),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
刷新订阅 TMDB 信息
@@ -472,7 +415,7 @@ def check_subscribes(
@router.get("/search", summary="搜索所有订阅", response_model=_SchemaResponse[None])
async def search_subscribes(
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
搜索所有订阅
@@ -492,7 +435,7 @@ async def search_subscribes(
async def search_subscribe(
subscribe_id: int,
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据订阅编号搜索订阅
@@ -518,7 +461,7 @@ async def delete_subscribe_by_media_identity(
command: DeleteSubscriptionsByIdentityCommand = Depends(
get_delete_subscriptions_by_identity_command
),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据任意媒体数据源 ID 删除订阅
@@ -616,28 +559,18 @@ async def subscribe_history(
mtype: str,
page: Optional[int] = 1,
count: Optional[int] = 30,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询电影电视剧或音乐订阅历史
"""
if current_user.is_superuser:
histories = await SubscribeHistory.async_list_by_type(
db, mtype=mtype, page=page, count=count
)
else:
histories = await SubscribeHistory.async_list_by_type_and_username(
db, mtype=mtype, username=current_user.name, page=page, count=count
)
result = []
for history in histories:
history_item = _SchemaSubscribe.model_validate(history, from_attributes=True)
if history_item.type == MediaType.TV.value:
history_item.total_episode = 0
history_item.lack_episode = 0
result.append(history_item)
return result
return await query.list_history(
mtype,
page=page,
count=count,
username=None if current_user.is_superuser else current_user.name,
)
@router.delete(
@@ -645,15 +578,17 @@ async def subscribe_history(
)
async def delete_subscribe_history(
history_id: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
删除订阅历史
"""
history = await SubscribeHistory.async_get(db, history_id)
if can_access_subscribe(history, current_user):
await SubscribeHistory.async_delete(db, history_id)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
await mutation.delete_history(history_id, actor)
return _SchemaResponse(success=True)
@@ -721,15 +656,15 @@ async def popular_subscribes(
)
async def user_subscribes(
username: str,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
查询用户订阅
"""
if not current_user.is_superuser and username != current_user.name:
return []
return await Subscribe.async_list_by_username(db, username)
return await query.list_public(username)
@router.get(
@@ -739,13 +674,17 @@ async def user_subscribes(
)
def subscribe_files(
subscribe_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
mutation: SubscriptionMutationService = Depends(get_subscription_sync_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user),
) -> Any:
"""
订阅相关文件信息
"""
subscribe = get_accessible_subscribe_sync(db, subscribe_id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = mutation.get_accessible_sync(subscribe_id, actor)
if subscribe:
return SubscribeChain().subscribe_files_info(subscribe)
return _SchemaSubscrbieInfo()
@@ -754,13 +693,17 @@ def subscribe_files(
@router.post("/share", summary="分享订阅", response_model=_SchemaResponse[None])
async def subscribe_share(
sub: _SchemaSubscribeShare,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
分享订阅
"""
subscribe = await get_accessible_subscribe(db, sub.subscribe_id, current_user)
actor = SubscriptionActor(
name=current_user.name,
is_superuser=current_user.is_superuser,
)
subscribe = await mutation.get_accessible(sub.subscribe_id, actor)
if not subscribe:
return _SchemaResponse(success=False, message="订阅不存在")
state, errmsg = await MoviePilotServerHelper.async_sub_share(
@@ -786,7 +729,7 @@ async def subscribe_share_delete(
@router.post("/fork", summary="复用订阅", response_model=_SchemaResponse[None])
async def subscribe_fork(
sub: _SchemaSubscribeShare,
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
复用订阅
@@ -809,7 +752,7 @@ async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token)) -
"""
查询已Follow的订阅分享人
"""
return SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
return get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
@router.post("/follow", summary="Follow订阅分享人", response_model=_SchemaResponse[None])
@@ -819,10 +762,10 @@ async def follow_subscriber(
"""
Follow订阅分享人
"""
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
if share_uid and share_uid not in subscribers:
subscribers.append(share_uid)
await SystemConfigOper().async_set(
await get_configured_system_config().async_set(
SystemConfigKey.FollowSubscribers, subscribers
)
return _SchemaResponse(success=True)
@@ -837,10 +780,10 @@ async def unfollow_subscriber(
"""
取消Follow订阅分享人
"""
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
if share_uid and share_uid in subscribers:
subscribers.remove(share_uid)
await SystemConfigOper().async_set(
await get_configured_system_config().async_set(
SystemConfigKey.FollowSubscribers, subscribers
)
return _SchemaResponse(success=True)
@@ -891,23 +834,27 @@ async def subscribe_share_statistics(
@router.get("/{subscribe_id}", summary="订阅详情", response_model=_SchemaSubscribe)
async def read_subscribe(
subscribe_id: int,
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_user_async),
query: SubscriptionQueryService = Depends(get_subscription_query_service),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
根据订阅编号查询订阅信息
"""
if not subscribe_id:
return Subscribe()
subscribe = await get_accessible_subscribe(db, subscribe_id, current_user)
return subscribe if subscribe else Subscribe()
return _SchemaSubscribe()
subscribe = await query.get_public(subscribe_id)
return (
subscribe
if subscribe and can_access_subscribe(subscribe, current_user)
else _SchemaSubscribe()
)
@router.delete("/{subscribe_id}", summary="删除订阅", response_model=_SchemaResponse[None])
async def delete_subscribe(
subscribe_id: int,
command: DeleteSubscribeCommand = Depends(get_delete_subscribe_command),
current_user: User = Depends(get_current_active_user_async),
current_user: ApiPrincipal = Depends(get_current_active_user_async),
) -> Any:
"""
删除订阅信息
+20 -20
View File
@@ -38,10 +38,10 @@ from app.chain.system import SystemChain
from app.runtime.config import global_vars, settings
from app.runtime.events import eventmanager
from app.domain.metainfo import MetaInfo
from app.runtime.extensions.module_manager import ModuleManager
from app.application.security.access import verify_apitoken, verify_resource_token, verify_token
from app.db.models import User
from app.db.oper.systemconfig import SystemConfigOper
from app.application.module import ModuleManager
from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.deps import get_current_active_superuser, get_current_active_superuser_async, get_current_active_user_async
from app.application.image import ImageHelper
from app.runtime.localization import LocaleHelper
@@ -57,7 +57,7 @@ from app.application.rules import RuleHelper
from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.state import SystemHelper
from app.runtime.log import logger
from app.scheduler import Scheduler
from app.application.scheduling import Scheduler
from app.schemas.event import ConfigChangeEventData
from app.schemas.types import SystemConfigKey, EventType
from app.foundation.crypto import HashUtils
@@ -704,7 +704,7 @@ def get_global_setting(token: str):
summary="查询用户相关系统设置",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def get_user_global_setting(_: User = Depends(get_current_active_user_async)):
async def get_user_global_setting(_: ApiPrincipal = Depends(get_current_active_user_async)):
"""
查询用户相关系统设置登录后获取
包含业务功能相关的配置和用户权限信息
@@ -745,7 +745,7 @@ async def get_user_global_setting(_: User = Depends(get_current_active_user_asyn
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def get_env_setting(
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""
查询系统环境变量包括当前版本号仅管理员
@@ -769,7 +769,7 @@ async def get_env_setting(
summary="查询安装版本统计报表",
response_model=_SchemaResponse[_SchemaJsonObject],
)
async def usage_statistic(_: User = Depends(get_current_active_user_async)):
async def usage_statistic(_: ApiPrincipal = Depends(get_current_active_user_async)):
"""
查询安装版本统计报表
"""
@@ -777,7 +777,7 @@ async def usage_statistic(_: User = Depends(get_current_active_user_async)):
@router.get("/ping", summary="服务存活检测", response_model=_SchemaResponse[None])
async def ping(_: User = Depends(get_current_active_user_async)) -> _SchemaResponse:
async def ping(_: ApiPrincipal = Depends(get_current_active_user_async)) -> _SchemaResponse:
"""
检测服务是否可用
"""
@@ -790,7 +790,7 @@ async def ping(_: User = Depends(get_current_active_user_async)) -> _SchemaRespo
response_model=_SchemaResponse[_SchemaSystemEnvironmentUpdateData],
)
async def set_env_setting(
env: dict, _: User = Depends(get_current_active_superuser_async)
env: dict, _: ApiPrincipal = Depends(get_current_active_superuser_async)
):
"""
更新系统环境变量仅管理员
@@ -870,7 +870,7 @@ async def get_progress(
response_model=_SchemaResponse[_SchemaValueData],
)
async def get_public_setting(
key: str, _: User = Depends(get_current_active_user_async)
key: str, _: ApiPrincipal = Depends(get_current_active_user_async)
) -> _SchemaResponse:
"""
查询普通用户可读取的非敏感系统设置
@@ -879,7 +879,7 @@ async def get_public_setting(
return _SchemaResponse(success=True, data={"value": getattr(settings, key)})
if key not in _PUBLIC_SYSTEM_CONFIG_KEYS:
raise HTTPException(status_code=404, detail="配置项不存在")
value = SystemConfigOper().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key])
value = get_configured_system_config().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key])
return _SchemaResponse(success=True, data={"value": value})
@@ -890,7 +890,7 @@ async def get_public_setting(
)
async def sync_plugin_market_from_wiki(
request: Optional[_SchemaPluginMarketSyncRequest] = Body(default=None),
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""
Wiki 插件文档同步插件市场仓库地址
@@ -956,7 +956,7 @@ async def sync_plugin_market_from_wiki(
response_model=_SchemaResponse[_SchemaValueData],
)
async def get_setting(
key: str, _: User = Depends(get_current_active_superuser_async)
key: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
) -> _SchemaResponse:
"""
查询系统设置仅管理员
@@ -964,7 +964,7 @@ async def get_setting(
if hasattr(settings, key):
value = getattr(settings, key)
else:
value = SystemConfigOper().get(key)
value = get_configured_system_config().get(key)
return _SchemaResponse(success=True, data={"value": value})
@@ -972,7 +972,7 @@ async def get_setting(
async def set_setting(
key: str,
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
_: User = Depends(get_current_active_superuser_async),
_: ApiPrincipal = Depends(get_current_active_superuser_async),
):
"""
更新系统设置仅管理员
@@ -992,7 +992,7 @@ async def set_setting(
if isinstance(value, list):
value = list(filter(None, value))
value = value if value else None
success = await SystemConfigOper().async_set(key, value)
success = await get_configured_system_config().async_set(key, value)
if success:
# 发送配置变更事件
await eventmanager.async_send_event(
@@ -1446,7 +1446,7 @@ def moduletest(moduleid: str, _: _SchemaTokenPayload = Depends(verify_token)):
@router.get("/restart", summary="重启系统", response_model=_SchemaResponse[None])
def restart_system(_: User = Depends(get_current_active_superuser)):
def restart_system(_: ApiPrincipal = Depends(get_current_active_superuser)):
"""
重启系统仅管理员
"""
@@ -1459,7 +1459,7 @@ def restart_system(_: User = Depends(get_current_active_superuser)):
@router.post("/upgrade", summary="升级并重启系统", response_model=_SchemaResponse[None])
def upgrade_system(
mode: Annotated[str | None, Body()] = None,
_: User = Depends(get_current_active_superuser),
_: ApiPrincipal = Depends(get_current_active_superuser),
):
"""
触发系统升级并重启仅管理员
@@ -1475,7 +1475,7 @@ def upgrade_system(
@router.get("/runscheduler", summary="运行服务", response_model=_SchemaResponse[None])
def run_scheduler(jobid: str, _: User = Depends(get_current_active_superuser)):
def run_scheduler(jobid: str, _: ApiPrincipal = Depends(get_current_active_superuser)):
"""
执行命令仅管理员
"""
+6 -7
View File
@@ -12,9 +12,8 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.tmdb import TmdbChain
from app.runtime.config import settings
from app.application.security.access import verify_token
from app.db.models.user import User
from app.db.oper.systemconfig import SystemConfigOper
from app.adapters.web.security.access import verify_token
from app.application.configuration import get_configured_system_config
from app.api.deps import get_current_active_superuser_async
from app.schemas.types import MediaType, SystemConfigKey
@@ -27,7 +26,7 @@ router = ResponseAPIRouter()
response_model=_SchemaResponse[_SchemaTmdbRecognitionCacheData],
)
async def tmdb_recognition_cache(
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""查询可管理的 TheMovieDb 识别缓存。"""
cache_items = TmdbChain().cache_items()
@@ -38,7 +37,7 @@ async def tmdb_recognition_cache(
"count": len(cache_items),
"recognized": recognized_count,
"unrecognized": len(cache_items) - recognized_count,
"shared_recognized": SystemConfigOper().get(
"shared_recognized": get_configured_system_config().get(
SystemConfigKey.MediaRecognizeShareCount
) or 0,
"shared_recognize_enabled": settings.MEDIA_RECOGNIZE_SHARE,
@@ -54,7 +53,7 @@ async def tmdb_recognition_cache(
)
async def delete_tmdb_recognition_cache(
cache_key: str,
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""按缓存键删除单条 TheMovieDb 识别缓存。"""
deleted_item = TmdbChain().delete_cache(cache_key)
@@ -67,7 +66,7 @@ async def delete_tmdb_recognition_cache(
"/cache", summary="清空 TheMovieDb 识别缓存", response_model=_SchemaResponse[None]
)
async def clear_tmdb_recognition_cache(
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
) -> _SchemaResponse:
"""清空全部 TheMovieDb 识别缓存。"""
TmdbChain().clear_cache()
+5 -6
View File
@@ -12,7 +12,6 @@ from app.runtime.config import settings
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.db.models import User
from app.api.deps import get_current_active_superuser, get_current_active_superuser_async
from app.schemas.types import (
MUSIC_ENTITY_RECORDING,
@@ -32,7 +31,7 @@ router = ResponseAPIRouter()
summary="获取种子缓存",
response_model=_SchemaResponse[_SchemaTorrentCacheData],
)
async def torrents_cache(_: User = Depends(get_current_active_superuser_async)):
async def torrents_cache(_: object = Depends(get_current_active_superuser_async)):
"""
获取当前种子缓存数据
"""
@@ -103,7 +102,7 @@ async def torrents_cache(_: User = Depends(get_current_active_superuser_async)):
async def delete_cache(
domain: str,
torrent_hash: str,
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
):
"""
删除指定的种子缓存
@@ -147,7 +146,7 @@ async def delete_cache(
@router.delete("/cache", summary="清理种子缓存", response_model=_SchemaResponse[None])
async def clear_cache(_: User = Depends(get_current_active_superuser_async)):
async def clear_cache(_: object = Depends(get_current_active_superuser_async)):
"""
清理所有种子缓存
"""
@@ -161,7 +160,7 @@ async def clear_cache(_: User = Depends(get_current_active_superuser_async)):
@router.post("/cache/refresh", summary="刷新种子缓存", response_model=_SchemaResponse[None])
def refresh_cache(_: User = Depends(get_current_active_superuser)):
def refresh_cache(_: object = Depends(get_current_active_superuser)):
"""
刷新种子缓存
"""
@@ -195,7 +194,7 @@ async def reidentify_cache(
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
music_type: Optional[MusicTargetEntityType] = None,
_: User = Depends(get_current_active_superuser_async),
_: object = Depends(get_current_active_superuser_async),
):
"""
重新识别指定的种子
+29 -22
View File
@@ -2,7 +2,6 @@ from pathlib import Path
from typing import Any, List, Annotated, Optional
from fastapi import Depends
from sqlalchemy.orm import Session
from app.schemas.common import NameData as _SchemaNameData
from app.schemas.response import Response as _SchemaResponse
@@ -19,12 +18,13 @@ from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.transfer import TransferChain
from app.runtime.config import settings, global_vars
from app.application.security.access import verify_token, verify_apitoken
from app.db import get_db
from app.db.models import User
from app.db.models.transferhistory import TransferHistory
from app.api.deps import get_current_active_manage_user
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.deps import (
get_current_active_manage_user,
get_transfer_history_lookup_service,
)
from app.application.directory import DirectoryHelper
from app.application.history import TransferHistoryLookupService
from app.runtime.log import logger
from app.schemas.types import MediaType
from app.schemas.workflow import FileItem
@@ -106,7 +106,8 @@ async def remove_queue(
def _resolve_manual_transfer_source_fileitems(
transer_item: ManualTransferItem, db: Session
transer_item: ManualTransferItem,
history_query: TransferHistoryLookupService,
) -> tuple[List[FileItem], Optional[str]]:
"""
从手动整理请求中解析源文件项
@@ -114,7 +115,7 @@ def _resolve_manual_transfer_source_fileitems(
if transer_item.logids:
fileitems: List[FileItem] = []
for logid in transer_item.logids:
history: TransferHistory = TransferHistory.get(db, logid)
history = history_query.get(logid)
if not history:
return [], f"整理记录不存在,ID{logid}"
if history.status and ("move" in history.mode):
@@ -124,7 +125,7 @@ def _resolve_manual_transfer_source_fileitems(
return fileitems, None
if transer_item.logid:
history: TransferHistory = TransferHistory.get(db, transer_item.logid)
history = history_query.get(transer_item.logid)
if not history:
return [], f"整理记录不存在,ID{transer_item.logid}"
if history.status and ("move" in history.mode):
@@ -195,19 +196,21 @@ def _get_manual_transfer_target_key(
)
def match_manual_transfer_target_path(
transer_item: ManualTransferItem,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
history_query: TransferHistoryLookupService = Depends(
get_transfer_history_lookup_service
),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
根据源文件匹配手动整理目的路径
:param transer_item: 手工整理项
:param db: 数据库
:param history_query: 整理历史投影服务
:param _: Token校验
"""
src_fileitems, error_message = _resolve_manual_transfer_source_fileitems(
transer_item=transer_item,
db=db,
history_query=history_query,
)
if error_message:
return _SchemaResponse(success=False, message=error_message)
@@ -258,19 +261,21 @@ def match_manual_transfer_target_path(
)
def query_manual_transfer_history(
transer_item: ManualTransferItem,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
history_query: TransferHistoryLookupService = Depends(
get_transfer_history_lookup_service
),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
查询文件或目录命中的成功整理记录
:param transer_item: 手工整理项
:param db: 数据库
:param history_query: 整理历史投影服务
:param _: Token校验
"""
src_fileitems, error_message = _resolve_manual_transfer_source_fileitems(
transer_item=transer_item,
db=db,
history_query=history_query,
)
if error_message:
return _SchemaResponse(success=False, message=error_message)
@@ -293,14 +298,16 @@ def query_manual_transfer_history(
def manual_transfer(
transer_item: ManualTransferItem,
background: Optional[bool] = False,
db: Session = Depends(get_db),
_: User = Depends(get_current_active_manage_user),
history_query: TransferHistoryLookupService = Depends(
get_transfer_history_lookup_service
),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
手动转移文件或历史记录支持自定义剧集识别格式
:param transer_item: 手工整理项
:param background: 后台运行
:param db: 数据库
:param history_query: 整理历史投影服务
:param _: Token校验
"""
force = False
@@ -311,7 +318,7 @@ def manual_transfer(
target_path = Path(transer_item.target_path) if transer_item.target_path else None
if transer_item.logid:
# 查询历史记录
history: TransferHistory = TransferHistory.get(db, transer_item.logid)
history = history_query.get(transer_item.logid)
if not history:
return _SchemaResponse(
success=False, message=f"整理记录不存在,ID{transer_item.logid}"
@@ -592,7 +599,7 @@ def manual_transfer(
)
def recommend_episode_format(
recommend_item: EpisodeFormatRecommendItem,
_: User = Depends(get_current_active_manage_user),
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
根据目录样本推荐集数定位模板
+45 -38
View File
@@ -3,7 +3,6 @@ import re
from typing import Annotated, Any, List, Union
from fastapi import Body, Depends, HTTPException, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.common import FileNameData as _SchemaFileNameData
from app.schemas.common import ValueData as _SchemaValueData
@@ -12,37 +11,41 @@ from app.schemas.user import User as _SchemaUser
from app.schemas.user import UserCreate as _SchemaUserCreate
from app.schemas.user import UserUpdate as _SchemaUserUpdate
from app.api.response import ResponseAPIRouter
from app.application.security.access import PasswordTooLongError, get_password_hash
from app.db import get_async_db
from app.db.models.user import User
from app.api.deps import get_current_active_superuser_async, get_current_active_user_async, get_current_active_user
from app.db.oper.userconfig import UserConfigOper
from app.application.security.token import PasswordTooLongError, get_password_hash
from app.application.security.user import UserService
from app.api.deps import (
get_current_active_superuser_async,
get_current_active_user_async,
get_current_active_user,
get_user_service,
)
from app.application.security.userconfig import get_configured_user_configuration
router = ResponseAPIRouter()
@router.get("/", summary="所有用户", response_model=List[_SchemaUser])
async def list_users(
db: AsyncSession = Depends(get_async_db),
current_user: User = Depends(get_current_active_superuser_async),
service: UserService = Depends(get_user_service),
current_user: Any = Depends(get_current_active_superuser_async),
) -> Any:
"""
查询用户列表
"""
return await current_user.async_list(db)
return await service.list()
@router.post("/", summary="新增用户", response_model=_SchemaResponse[None])
async def create_user(
*,
db: AsyncSession = Depends(get_async_db),
service: UserService = Depends(get_user_service),
user_in: _SchemaUserCreate,
current_user: User = Depends(get_current_active_superuser_async),
current_user: Any = Depends(get_current_active_superuser_async),
) -> Any:
"""
新增用户
"""
user = await current_user.async_get_by_name(db, name=user_in.name)
user = await service.get_by_name(user_in.name)
if user:
return _SchemaResponse(success=False, message="用户已存在")
user_info = user_in.model_dump()
@@ -52,16 +55,16 @@ async def create_user(
except PasswordTooLongError as error:
return _SchemaResponse(success=False, message=str(error))
user_info.pop("password")
user = await User(**user_info).async_create(db)
user = await service.create(user_info)
return _SchemaResponse(success=True if user else False)
@router.put("/", summary="更新用户", response_model=_SchemaResponse[None])
async def update_user(
*,
db: AsyncSession = Depends(get_async_db),
service: UserService = Depends(get_user_service),
user_in: _SchemaUserUpdate,
current_user: User = Depends(get_current_active_superuser_async),
current_user: Any = Depends(get_current_active_superuser_async),
) -> Any:
"""
更新用户
@@ -80,24 +83,24 @@ async def update_user(
except PasswordTooLongError as error:
return _SchemaResponse(success=False, message=str(error))
user_info.pop("password")
user = await current_user.async_get_by_id(db, user_id=user_info["id"])
user = await service.get_by_id(user_info["id"])
user_name = user_info.get("name")
if not user_name:
return _SchemaResponse(success=False, message="用户名不能为空")
# 新用户名去重
users = await current_user.async_list(db)
users = await service.list()
for u in users:
if u.name == user_name and u.id != user_info["id"]:
return _SchemaResponse(success=False, message="用户名已被使用")
if not user:
return _SchemaResponse(success=False, message="用户不存在")
await user.async_update(db, user_info)
await service.update(user_info["id"], user_info)
return _SchemaResponse(success=True)
@router.get("/current", summary="当前登录用户信息", response_model=_SchemaUser)
async def read_current_user(
current_user: User = Depends(get_current_active_user_async),
current_user: Any = Depends(get_current_active_user_async),
) -> Any:
"""
当前登录用户信息
@@ -112,9 +115,9 @@ async def read_current_user(
)
async def upload_avatar(
user_id: int,
db: AsyncSession = Depends(get_async_db),
service: UserService = Depends(get_user_service),
file: UploadFile = File(...),
current_user: User = Depends(get_current_active_user_async),
current_user: Any = Depends(get_current_active_user_async),
) -> _SchemaResponse:
"""
上传用户头像
@@ -125,10 +128,10 @@ async def upload_avatar(
# 将文件转换为Base64
file_base64 = base64.b64encode(file.file.read())
# 更新到用户表
user = await User.async_get(db, user_id)
user = await service.get_by_id(user_id)
if not user:
return _SchemaResponse(success=False, message="用户不存在")
await user.async_update(db, {"avatar": f"data:image/ico;base64,{file_base64}"})
await service.update(user_id, {"avatar": f"data:image/ico;base64,{file_base64}"})
return _SchemaResponse(success=True, data={"filename": file.filename})
@@ -137,11 +140,11 @@ async def upload_avatar(
summary="查询用户配置",
response_model=_SchemaResponse[_SchemaValueData],
)
def get_config(key: str, current_user: User = Depends(get_current_active_user)):
def get_config(key: str, current_user: Any = Depends(get_current_active_user)):
"""
查询用户配置
"""
value = UserConfigOper().get(username=current_user.name, key=key)
value = get_configured_user_configuration().get(username=current_user.name, key=key)
return _SchemaResponse(success=True, data={"value": value})
@@ -149,59 +152,63 @@ def get_config(key: str, current_user: User = Depends(get_current_active_user)):
def set_config(
key: str,
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
current_user: User = Depends(get_current_active_user),
current_user: Any = Depends(get_current_active_user),
):
"""
更新用户配置
"""
UserConfigOper().set(username=current_user.name, key=key, value=value)
get_configured_user_configuration().set(
username=current_user.name,
key=key,
value=value,
)
return _SchemaResponse(success=True)
@router.delete("/id/{user_id}", summary="删除用户", response_model=_SchemaResponse[None])
async def delete_user_by_id(
*,
db: AsyncSession = Depends(get_async_db),
service: UserService = Depends(get_user_service),
user_id: int,
current_user: User = Depends(get_current_active_superuser_async),
current_user: Any = Depends(get_current_active_superuser_async),
) -> Any:
"""
通过唯一ID删除用户
"""
user = await current_user.async_get_by_id(db, user_id=user_id)
user = await service.get_by_id(user_id)
if not user:
return _SchemaResponse(success=False, message="用户不存在")
await current_user.async_delete(db, user_id)
await service.delete(user_id)
return _SchemaResponse(success=True)
@router.delete("/name/{user_name}", summary="删除用户", response_model=_SchemaResponse[None])
async def delete_user_by_name(
*,
db: AsyncSession = Depends(get_async_db),
service: UserService = Depends(get_user_service),
user_name: str,
current_user: User = Depends(get_current_active_superuser_async),
current_user: Any = Depends(get_current_active_superuser_async),
) -> Any:
"""
通过用户名删除用户
"""
user = await current_user.async_get_by_name(db, name=user_name)
user = await service.get_by_name(user_name)
if not user:
return _SchemaResponse(success=False, message="用户不存在")
await current_user.async_delete(db, user.id)
await service.delete(user.id)
return _SchemaResponse(success=True)
@router.get("/{username}", summary="用户详情", response_model=_SchemaUser)
async def read_user_by_name(
username: str,
current_user: User = Depends(get_current_active_user_async),
db: AsyncSession = Depends(get_async_db),
current_user: Any = Depends(get_current_active_user_async),
service: UserService = Depends(get_user_service),
) -> Any:
"""
查询用户详情
"""
user = await current_user.async_get_by_name(db, name=username)
user = await service.get_by_name(username)
if not user:
raise HTTPException(
status_code=404,
+1 -1
View File
@@ -5,7 +5,7 @@ from fastapi import BackgroundTasks, Request, Depends
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.webhook import WebhookChain
from app.application.security.access import verify_apitoken
from app.adapters.web.security.access import verify_apitoken
router = ResponseAPIRouter()
+27 -26
View File
@@ -1,7 +1,6 @@
from typing import List, Any, Optional
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.response import Response as _SchemaResponse
from app.schemas.workflow import NameValueOption as _SchemaNameValueOption
@@ -10,19 +9,21 @@ from app.schemas.workflow import Workflow as _SchemaWorkflow
from app.schemas.workflow import WorkflowActionDefinition as _SchemaWorkflowActionDefinition
from app.schemas.workflow import WorkflowShare as _SchemaWorkflowShare
from app.api.response import ResponseAPIRouter
from app.application.workflow import WorkflowDefinitionCommand, WorkflowMutationCommand
from app.application.workflow import (
WorkflowDefinitionCommand,
WorkflowMutationCommand,
WorkflowQueryService,
)
from app.chain.workflow import WorkflowChain
from app.runtime.extensions.plugin_manager import PluginManager
from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.workflow import WorkFlowManager
from app.db import get_async_db
from app.db.models import User
from app.api.deps import (
get_current_active_manage_user,
get_current_active_manage_user_async,
get_workflow_definition_command,
get_workflow_mutation_command,
get_workflow_query_service,
)
from app.db.oper.workflow import WorkflowOper
from app.adapters.external.server import MoviePilotServerHelper
from app.schemas.types import EventType, EVENT_TYPE_NAMES
@@ -30,20 +31,20 @@ router = ResponseAPIRouter()
@router.get("/", summary="所有工作流", response_model=List[_SchemaWorkflow])
async def list_workflows(
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: WorkflowQueryService = Depends(get_workflow_query_service),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
获取工作流列表
"""
return await WorkflowOper(db).async_list()
return await query.list()
@router.post("/", summary="创建工作流", response_model=_SchemaResponse[None])
async def create_workflow(
workflow: _SchemaWorkflow,
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
_: User = Depends(get_current_active_manage_user_async),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
创建工作流
@@ -58,7 +59,7 @@ async def create_workflow(
response_model=List[_SchemaPluginWorkflowActionGroup],
)
def list_plugin_actions(
plugin_id: str = None, _: User = Depends(get_current_active_manage_user)
plugin_id: str = None, _: Any = Depends(get_current_active_manage_user)
) -> Any:
"""
获取所有动作
@@ -71,7 +72,7 @@ def list_plugin_actions(
summary="所有动作",
response_model=List[_SchemaWorkflowActionDefinition],
)
async def list_actions(_: User = Depends(get_current_active_manage_user_async)) -> Any:
async def list_actions(_: Any = Depends(get_current_active_manage_user_async)) -> Any:
"""
获取所有动作
"""
@@ -83,7 +84,7 @@ async def list_actions(_: User = Depends(get_current_active_manage_user_async))
summary="获取所有事件类型",
response_model=List[_SchemaNameValueOption],
)
async def get_event_types(_: User = Depends(get_current_active_manage_user_async)) -> Any:
async def get_event_types(_: Any = Depends(get_current_active_manage_user_async)) -> Any:
"""
获取所有事件类型
"""
@@ -98,7 +99,7 @@ async def get_event_types(_: User = Depends(get_current_active_manage_user_async
@router.post("/share", summary="分享工作流", response_model=_SchemaResponse[None])
async def workflow_share(
workflow: _SchemaWorkflowShare, _: User = Depends(get_current_active_manage_user_async)
workflow: _SchemaWorkflowShare, _: Any = Depends(get_current_active_manage_user_async)
) -> Any:
"""
分享工作流
@@ -119,7 +120,7 @@ async def workflow_share(
@router.delete("/share/{share_id}", summary="删除分享", response_model=_SchemaResponse[None])
async def workflow_share_delete(
share_id: int, _: User = Depends(get_current_active_manage_user_async)
share_id: int, _: Any = Depends(get_current_active_manage_user_async)
) -> Any:
"""
删除分享
@@ -132,7 +133,7 @@ async def workflow_share_delete(
async def workflow_fork(
workflow: _SchemaWorkflowShare,
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
_: User = Depends(get_current_active_manage_user_async),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
复用工作流
@@ -148,7 +149,7 @@ async def workflow_shares(
name: Optional[str] = None,
page: Optional[int] = 1,
count: Optional[int] = 30,
_: User = Depends(get_current_active_manage_user_async),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
查询分享的工作流
@@ -162,7 +163,7 @@ async def workflow_shares(
def run_workflow(
workflow_id: int,
from_begin: Optional[bool] = True,
_: User = Depends(get_current_active_manage_user),
_: Any = Depends(get_current_active_manage_user),
) -> Any:
"""
执行工作流
@@ -179,7 +180,7 @@ def run_workflow(
def start_workflow(
workflow_id: int,
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
_: User = Depends(get_current_active_manage_user),
_: Any = Depends(get_current_active_manage_user),
) -> Any:
"""
启用工作流
@@ -194,7 +195,7 @@ def start_workflow(
def pause_workflow(
workflow_id: int,
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
_: User = Depends(get_current_active_manage_user),
_: Any = Depends(get_current_active_manage_user),
) -> Any:
"""
停用工作流
@@ -209,7 +210,7 @@ def pause_workflow(
async def reset_workflow(
workflow_id: int,
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
_: User = Depends(get_current_active_manage_user_async),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
重置工作流
@@ -221,20 +222,20 @@ async def reset_workflow(
@router.get("/{workflow_id}", summary="工作流详情", response_model=_SchemaWorkflow)
async def get_workflow(
workflow_id: int,
db: AsyncSession = Depends(get_async_db),
_: User = Depends(get_current_active_manage_user_async),
query: WorkflowQueryService = Depends(get_workflow_query_service),
_: Any = Depends(get_current_active_manage_user_async),
) -> Any:
"""
获取工作流详情
"""
return await WorkflowOper(db).async_get(workflow_id)
return await query.get(workflow_id)
@router.put("/{workflow_id}", summary="更新工作流", response_model=_SchemaResponse[None])
def update_workflow(
workflow: _SchemaWorkflow,
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
_: User = Depends(get_current_active_manage_user),
_: Any = Depends(get_current_active_manage_user),
) -> Any:
"""
更新工作流
@@ -247,7 +248,7 @@ def update_workflow(
def delete_workflow(
workflow_id: int,
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
_: User = Depends(get_current_active_manage_user),
_: Any = Depends(get_current_active_manage_user),
) -> Any:
"""
删除工作流