mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor(db): 修复异步连接池无界增长,并完成 SQLAlchemy 2.0 迁移与分层归位 (#6320)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
数据访问层(Oper)。
|
||||
|
||||
与 app/db/models 一一对应:models 声明表结构,oper 承载针对该表的读写。
|
||||
两个包同名文件互为镜像(models/subscribe.py ↔ oper/subscribe.py),
|
||||
文件名只写实体,角色由包名表达,因此这里不再有 `_oper` 后缀。
|
||||
|
||||
本文件只做符号解析,不在 import 期执行任何动作——没有建引擎、没有连库、
|
||||
也不会把十六个 Oper 模块一并拉起。`from app.db.oper import SubscribeOper`
|
||||
经下方 __getattr__ 惰性解析,只导入被点名的那一个模块。
|
||||
|
||||
这一点不是洁癖:多处测试靠往 sys.modules 塞桩来隔离单个 Oper(例如
|
||||
app.db.oper.systemconfig),若本文件改成 models/__init__.py 那样的即时
|
||||
re-export,导入任意一个 Oper 都会连带把其余十五个真正拉起来,桩就被绕过了。
|
||||
按模块直连(from app.db.oper.subscribe import SubscribeOper)仍是仓库内的
|
||||
首选写法,本入口是给「只想要一个类名」的调用方备的门面。
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# 运行期由 __getattr__ 解析,模块 __dict__ 里并不存在这些名字;
|
||||
# 这里为静态检查补上真实类型,同时消掉 __all__ 的未定义告警。
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.db.oper.downloadfailure import DownloadFailureOper
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.plugindata import PluginDataOper
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.db.oper.transferpending import TransferPendingOper
|
||||
from app.db.oper.user import UserOper
|
||||
from app.db.oper.userconfig import UserConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
|
||||
# 类名 -> 所在子模块。子模块名即实体名,与 app/db/models 对齐。
|
||||
_OPER_MODULES = {
|
||||
"AgentChatOper": "agentchat",
|
||||
"AgentTaskOper": "agenttask",
|
||||
"DownloadFailureOper": "downloadfailure",
|
||||
"DownloadHistoryOper": "downloadhistory",
|
||||
"MediaServerOper": "mediaserver",
|
||||
"MessageOper": "message",
|
||||
"PluginDataOper": "plugindata",
|
||||
"SiteOper": "site",
|
||||
"SubscribeHistoryOper": "subscribehistory",
|
||||
"SubscribeOper": "subscribe",
|
||||
"SystemConfigOper": "systemconfig",
|
||||
"TransferHistoryOper": "transferhistory",
|
||||
"TransferPendingOper": "transferpending",
|
||||
"UserConfigOper": "userconfig",
|
||||
"UserOper": "user",
|
||||
"WorkflowOper": "workflow",
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""
|
||||
惰性解析 Oper 类,只导入被点名的那个子模块。
|
||||
:param name: 属性名
|
||||
:return: 对应的 Oper 类
|
||||
"""
|
||||
module_name = _OPER_MODULES.get(name)
|
||||
if module_name is None:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
from importlib import import_module
|
||||
|
||||
return getattr(import_module(f"{__name__}.{module_name}"), name)
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
"""
|
||||
让 dir() 与自动补全看得见惰性名字。
|
||||
:return: 属性名列表
|
||||
"""
|
||||
return sorted({*globals(), *_OPER_MODULES})
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentChatOper",
|
||||
"AgentTaskOper",
|
||||
"DownloadFailureOper",
|
||||
"DownloadHistoryOper",
|
||||
"MediaServerOper",
|
||||
"MessageOper",
|
||||
"PluginDataOper",
|
||||
"SiteOper",
|
||||
"SubscribeHistoryOper",
|
||||
"SubscribeOper",
|
||||
"SystemConfigOper",
|
||||
"TransferHistoryOper",
|
||||
"TransferPendingOper",
|
||||
"UserConfigOper",
|
||||
"UserOper",
|
||||
"WorkflowOper",
|
||||
]
|
||||
@@ -0,0 +1,344 @@
|
||||
import time
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.agentchat import AgentChat
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
DEFAULT_AGENT_CHAT_TITLE = "未命名会话"
|
||||
|
||||
|
||||
class AgentChatOper(DbOper):
|
||||
"""
|
||||
Agent 会话历史数据管理。
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
|
||||
super().__init__(db)
|
||||
|
||||
@staticmethod
|
||||
def _now() -> str:
|
||||
"""返回数据库统一使用的当前时间字符串。"""
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
|
||||
@staticmethod
|
||||
def _channel_value(channel: Optional[Union[MessageChannel, str]]) -> Optional[str]:
|
||||
"""获取渠道枚举的字符串值。"""
|
||||
if isinstance(channel, MessageChannel):
|
||||
return channel.value
|
||||
return channel
|
||||
|
||||
@staticmethod
|
||||
def _normalize_messages(messages: Optional[list[dict]]) -> list[dict]:
|
||||
"""规范化展示消息列表,避免 JSON 字段存入 None。"""
|
||||
return messages if isinstance(messages, list) else []
|
||||
|
||||
@staticmethod
|
||||
def _normalize_title(value: Optional[str], messages: list[dict]) -> str:
|
||||
"""生成会话标题。"""
|
||||
if value and value.strip():
|
||||
return value.strip()[:120]
|
||||
for message in messages:
|
||||
if message.get("role") != "user":
|
||||
continue
|
||||
content = str(message.get("content") or "").strip()
|
||||
if content:
|
||||
return content.replace("\n", " ")[:120]
|
||||
attachments = message.get("attachments")
|
||||
if isinstance(attachments, list) and attachments:
|
||||
name = attachments[0].get("name") or "附件消息"
|
||||
return str(name)[:120]
|
||||
return DEFAULT_AGENT_CHAT_TITLE
|
||||
|
||||
@staticmethod
|
||||
def has_custom_title(value: Optional[str]) -> bool:
|
||||
"""判断会话是否已有真实标题。"""
|
||||
return bool(value and value.strip() and value.strip() != DEFAULT_AGENT_CHAT_TITLE)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_preview(messages: list[dict]) -> str:
|
||||
"""生成会话预览文本。"""
|
||||
for message in reversed(messages):
|
||||
content = str(message.get("content") or "").strip()
|
||||
if content:
|
||||
return content.replace("\n", " ")[:240]
|
||||
attachments = message.get("attachments")
|
||||
if isinstance(attachments, list) and attachments:
|
||||
name = attachments[0].get("name") or "附件消息"
|
||||
return str(name)[:240]
|
||||
return ""
|
||||
|
||||
def get(
|
||||
self, session_id: str, user_id: Optional[str] = None
|
||||
) -> Optional[AgentChat]:
|
||||
"""
|
||||
获取 Agent 会话。
|
||||
"""
|
||||
return AgentChat.get_by_session(self._db, session_id, user_id)
|
||||
|
||||
async def async_get(
|
||||
self, session_id: str, user_id: Optional[str] = None
|
||||
) -> Optional[AgentChat]:
|
||||
"""
|
||||
异步获取 Agent 会话。
|
||||
"""
|
||||
return await AgentChat.async_get_by_session(self._db, session_id, user_id)
|
||||
|
||||
def ensure_session(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Union[MessageChannel, str]] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> Optional[AgentChat]:
|
||||
"""
|
||||
确保 Agent 会话记录存在,并刷新基础渠道信息。
|
||||
"""
|
||||
now = self._now()
|
||||
chat = self.get(session_id=session_id, user_id=user_id)
|
||||
if not chat:
|
||||
chat = self.get(session_id=session_id)
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"channel": self._channel_value(channel),
|
||||
"source": source,
|
||||
"original_chat_id": original_chat_id,
|
||||
"client_session_id": client_session_id,
|
||||
"updated_at": now,
|
||||
}
|
||||
payload = {key: value for key, value in payload.items() if value is not None}
|
||||
if chat:
|
||||
chat.update(self._db, payload)
|
||||
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
|
||||
|
||||
chat = AgentChat(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
channel=self._channel_value(channel),
|
||||
source=source,
|
||||
original_chat_id=original_chat_id,
|
||||
client_session_id=client_session_id,
|
||||
title=DEFAULT_AGENT_CHAT_TITLE,
|
||||
preview="",
|
||||
agent_messages=[],
|
||||
display_messages=[],
|
||||
message_count=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
chat.create(self._db)
|
||||
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
|
||||
|
||||
def save_agent_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str],
|
||||
messages: list[dict],
|
||||
) -> None:
|
||||
"""
|
||||
保存可恢复 Agent 上下文的原始消息。
|
||||
"""
|
||||
chat = self.get(session_id=session_id, user_id=user_id)
|
||||
if not chat:
|
||||
chat = self.get(session_id=session_id)
|
||||
if not chat:
|
||||
chat = self.ensure_session(session_id=session_id, user_id=user_id)
|
||||
if not chat:
|
||||
return
|
||||
chat.update(
|
||||
self._db,
|
||||
{
|
||||
"agent_messages": messages or [],
|
||||
"updated_at": self._now(),
|
||||
},
|
||||
)
|
||||
|
||||
def update_title_if_empty(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str],
|
||||
title: Optional[str],
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Union[MessageChannel, str]] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
在会话尚未生成标题时写入标题。
|
||||
"""
|
||||
normalized_title = self._normalize_title(title, [])
|
||||
if normalized_title == DEFAULT_AGENT_CHAT_TITLE:
|
||||
return
|
||||
|
||||
chat = self.ensure_session(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
channel=channel,
|
||||
source=source,
|
||||
original_chat_id=original_chat_id,
|
||||
client_session_id=client_session_id,
|
||||
)
|
||||
if not chat:
|
||||
return
|
||||
if self.has_custom_title(chat.title):
|
||||
return
|
||||
chat.update(
|
||||
self._db,
|
||||
{
|
||||
"title": normalized_title,
|
||||
"updated_at": self._now(),
|
||||
},
|
||||
)
|
||||
|
||||
def save_display_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
messages: Optional[list[dict]] = None,
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Union[MessageChannel, str]] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
) -> Optional[AgentChat]:
|
||||
"""
|
||||
保存用户可见的 Agent 会话消息。
|
||||
"""
|
||||
normalized_messages = self._normalize_messages(messages)
|
||||
chat = self.ensure_session(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
channel=channel,
|
||||
source=source,
|
||||
original_chat_id=original_chat_id,
|
||||
client_session_id=client_session_id,
|
||||
)
|
||||
if not chat:
|
||||
return None
|
||||
normalized_title = (
|
||||
chat.title
|
||||
if self.has_custom_title(chat.title)
|
||||
else self._normalize_title(title, normalized_messages)
|
||||
)
|
||||
chat.update(
|
||||
self._db,
|
||||
{
|
||||
"title": normalized_title,
|
||||
"preview": self._normalize_preview(normalized_messages),
|
||||
"display_messages": normalized_messages,
|
||||
"message_count": len(normalized_messages),
|
||||
"updated_at": self._now(),
|
||||
},
|
||||
)
|
||||
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
|
||||
|
||||
def append_display_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
messages: Optional[list[dict]] = None,
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Union[MessageChannel, str]] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> Optional[AgentChat]:
|
||||
"""
|
||||
追加一组用户可见的 Agent 会话消息。
|
||||
"""
|
||||
chat = self.ensure_session(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
channel=channel,
|
||||
source=source,
|
||||
original_chat_id=original_chat_id,
|
||||
client_session_id=client_session_id,
|
||||
)
|
||||
if not chat:
|
||||
return None
|
||||
display_messages = self._normalize_messages(chat.display_messages)
|
||||
display_messages.extend(self._normalize_messages(messages))
|
||||
title = chat.title if self.has_custom_title(chat.title) else None
|
||||
return self.save_display_messages(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
messages=display_messages,
|
||||
username=username or chat.username,
|
||||
channel=channel or chat.channel,
|
||||
source=source or chat.source,
|
||||
original_chat_id=original_chat_id or chat.original_chat_id,
|
||||
client_session_id=client_session_id or chat.client_session_id,
|
||||
title=title,
|
||||
)
|
||||
|
||||
async def async_list_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
user_id: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
) -> list[AgentChat]:
|
||||
"""
|
||||
异步分页获取 Agent 会话历史。
|
||||
"""
|
||||
return await AgentChat.async_list_by_page(
|
||||
self._db,
|
||||
page=page,
|
||||
count=count,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
)
|
||||
|
||||
async def async_delete(
|
||||
self, session_id: str, user_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
异步删除 Agent 会话历史。
|
||||
"""
|
||||
chat = await self.async_get(session_id=session_id, user_id=user_id)
|
||||
if not chat:
|
||||
return False
|
||||
await AgentChat.async_delete(self._db, chat.id)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def to_summary(chat: AgentChat) -> dict[str, Any]:
|
||||
"""
|
||||
转换为历史会话摘要。
|
||||
"""
|
||||
return {
|
||||
"id": chat.id,
|
||||
"session_id": chat.session_id,
|
||||
"client_session_id": chat.client_session_id,
|
||||
"title": chat.title,
|
||||
"channel": chat.channel,
|
||||
"source": chat.source,
|
||||
"user_id": chat.user_id,
|
||||
"username": chat.username,
|
||||
"original_chat_id": chat.original_chat_id,
|
||||
"message_count": chat.message_count or 0,
|
||||
"created_at": chat.created_at,
|
||||
"updated_at": chat.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def to_detail(cls, chat: AgentChat) -> dict[str, Any]:
|
||||
"""
|
||||
转换为历史会话详情。
|
||||
"""
|
||||
data = cls.to_summary(chat)
|
||||
data["messages"] = chat.display_messages or []
|
||||
return data
|
||||
@@ -0,0 +1,233 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.agenttask import AgentTask
|
||||
from app.db.models.agenttaskrun import AgentTaskRun
|
||||
|
||||
|
||||
class AgentTaskOper(DbOper):
|
||||
"""
|
||||
Agent 自主定时任务管理。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _now() -> str:
|
||||
"""生成当前数据库时间字符串。"""
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def add(self, **kwargs: object) -> Optional[AgentTask]:
|
||||
"""
|
||||
新增 Agent 定时任务。
|
||||
"""
|
||||
now = self._now()
|
||||
task_id = AgentTask.add_task(
|
||||
self._db,
|
||||
**kwargs,
|
||||
enabled=True,
|
||||
last_status="waiting",
|
||||
run_count=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
return self.get(task_id)
|
||||
|
||||
def get(
|
||||
self,
|
||||
task_id: int,
|
||||
user_id: Optional[str] = None,
|
||||
) -> Optional[AgentTask]:
|
||||
"""
|
||||
查询单个 Agent 定时任务。
|
||||
"""
|
||||
return AgentTask.get_for_user(self._db, task_id=task_id, user_id=user_id)
|
||||
|
||||
def list(
|
||||
self,
|
||||
user_id: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
) -> list[AgentTask]:
|
||||
"""
|
||||
查询 Agent 定时任务列表。
|
||||
"""
|
||||
return AgentTask.list_for_user(self._db, user_id=user_id, enabled=enabled)
|
||||
|
||||
def update(
|
||||
self,
|
||||
task_id: int,
|
||||
payload: dict,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
更新 Agent 定时任务。
|
||||
"""
|
||||
normalized_payload = {
|
||||
key: value
|
||||
for key, value in payload.items()
|
||||
if key in {
|
||||
"name",
|
||||
"content",
|
||||
"trigger_type",
|
||||
"cron_expression",
|
||||
"run_at",
|
||||
"enabled",
|
||||
"last_status",
|
||||
"last_result",
|
||||
}
|
||||
}
|
||||
if not normalized_payload:
|
||||
return False
|
||||
normalized_payload["updated_at"] = self._now()
|
||||
return AgentTask.update_task(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
payload=normalized_payload,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
def delete(self, task_id: int, user_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除非运行中的 Agent 定时任务及其运行历史。
|
||||
"""
|
||||
return AgentTaskRun.delete_task_and_runs(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
def begin_run(
|
||||
self,
|
||||
task_id: int,
|
||||
trigger_source: str = "scheduled",
|
||||
) -> Optional[AgentTaskRun]:
|
||||
"""
|
||||
原子创建一次运行并返回其任务快照。
|
||||
"""
|
||||
run_id = uuid4().hex
|
||||
created_run_id = AgentTaskRun.begin_run(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
run_id=run_id,
|
||||
trigger_source=trigger_source,
|
||||
started_at=self._now(),
|
||||
)
|
||||
return self.get_run(created_run_id) if created_run_id else None
|
||||
|
||||
def mark_running(self, task_id: int) -> bool:
|
||||
"""兼容既有调用并为该次执行创建运行记录。"""
|
||||
return self.begin_run(task_id=task_id) is not None
|
||||
|
||||
def mark_interrupted(self, task_id: int, result: str) -> bool:
|
||||
"""
|
||||
将遗留的运行中任务标记为中断且结果未知。
|
||||
"""
|
||||
return AgentTaskRun.interrupt_task(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
result=(result or "")[:20000],
|
||||
finished_at=self._now(),
|
||||
)
|
||||
|
||||
def get_run(self, run_id: str) -> Optional[AgentTaskRun]:
|
||||
"""查询一次 Agent 任务运行。"""
|
||||
return AgentTaskRun.get_by_run_id(self._db, run_id=run_id)
|
||||
|
||||
def list_runs(
|
||||
self,
|
||||
task_id: int,
|
||||
user_id: Optional[str] = None,
|
||||
limit: int = 10,
|
||||
) -> list[AgentTaskRun]:
|
||||
"""查询任务最近的有界运行历史。"""
|
||||
return AgentTaskRun.list_for_task(
|
||||
self._db,
|
||||
task_id=task_id,
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def finish_run(
|
||||
self,
|
||||
run_id: str,
|
||||
success: bool,
|
||||
result: str,
|
||||
disable_date_task: bool = False,
|
||||
) -> bool:
|
||||
"""收口精确运行并更新仍匹配的任务投影。"""
|
||||
return AgentTaskRun.finish_run(
|
||||
self._db,
|
||||
run_id=run_id,
|
||||
success=success,
|
||||
result=(result or "")[:20000],
|
||||
finished_at=self._now(),
|
||||
disable_date_task=disable_date_task,
|
||||
)
|
||||
|
||||
def finish(
|
||||
self,
|
||||
task_id: int,
|
||||
success: bool,
|
||||
result: str,
|
||||
disable: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
记录 Agent 定时任务执行结果。
|
||||
"""
|
||||
task = self.get(task_id)
|
||||
if not task or not task.last_run_id:
|
||||
return False
|
||||
return self.finish_run(
|
||||
run_id=task.last_run_id,
|
||||
success=success,
|
||||
result=result,
|
||||
disable_date_task=disable,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_dict(
|
||||
task: AgentTask,
|
||||
next_run_at: Optional[str] = None,
|
||||
timezone: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
将 Agent 定时任务转换为工具可返回的结构。
|
||||
"""
|
||||
return {
|
||||
"id": task.id,
|
||||
"name": task.name,
|
||||
"content": task.content,
|
||||
"trigger_type": task.trigger_type,
|
||||
"cron_expression": task.cron_expression,
|
||||
"run_at": task.run_at,
|
||||
"timezone": timezone,
|
||||
"enabled": bool(task.enabled),
|
||||
"last_status": task.last_status,
|
||||
"last_run_at": task.last_run_at,
|
||||
"last_result": task.last_result,
|
||||
"last_run_id": task.last_run_id,
|
||||
"run_count": task.run_count or 0,
|
||||
"next_run_at": next_run_at,
|
||||
"created_at": task.created_at,
|
||||
"updated_at": task.updated_at,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def run_to_dict(run: AgentTaskRun) -> dict:
|
||||
"""将一次 Agent 任务运行转换为工具返回结构。"""
|
||||
return {
|
||||
"run_id": run.run_id,
|
||||
"task_id": run.task_id,
|
||||
"trigger_source": run.trigger_source,
|
||||
"name": run.name,
|
||||
"content": run.content,
|
||||
"trigger_type": run.trigger_type,
|
||||
"cron_expression": run.cron_expression,
|
||||
"run_at": run.run_at,
|
||||
"status": run.status,
|
||||
"started_at": run.started_at,
|
||||
"finished_at": run.finished_at,
|
||||
"result": run.result,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.downloadfailure import DownloadFailure
|
||||
|
||||
|
||||
class DownloadFailureOper(DbOper):
|
||||
"""
|
||||
下载失败冷却记录管理。
|
||||
"""
|
||||
|
||||
def get_active_by_fingerprints(
|
||||
self,
|
||||
fingerprints: List[str],
|
||||
now_time: str,
|
||||
) -> Dict[str, DownloadFailure]:
|
||||
"""
|
||||
批量按指纹查询仍在冷却期的失败记录。
|
||||
"""
|
||||
failures = DownloadFailure.get_active_by_fingerprints(
|
||||
self._db,
|
||||
fingerprints=fingerprints,
|
||||
now_time=now_time,
|
||||
)
|
||||
return {
|
||||
failure.fingerprint: failure
|
||||
for failure in failures
|
||||
if failure and failure.fingerprint
|
||||
}
|
||||
|
||||
def record_failure(
|
||||
self,
|
||||
fingerprint: str,
|
||||
now_time: str,
|
||||
next_retry_at: str,
|
||||
**kwargs: object,
|
||||
) -> DownloadFailure:
|
||||
"""
|
||||
新增或更新资源失败记录。
|
||||
"""
|
||||
return DownloadFailure.record_failure(
|
||||
self._db,
|
||||
fingerprint=fingerprint,
|
||||
now_time=now_time,
|
||||
next_retry_at=next_retry_at,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def delete_expired(
|
||||
self,
|
||||
before_time: str,
|
||||
limit: Optional[int] = 500,
|
||||
) -> int:
|
||||
"""
|
||||
删除已过期较久的失败记录。
|
||||
"""
|
||||
return DownloadFailure.delete_expired(
|
||||
self._db,
|
||||
before_time=before_time,
|
||||
limit=limit,
|
||||
)
|
||||
@@ -0,0 +1,199 @@
|
||||
from typing import Dict, List, Optional, cast
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.downloadhistory import DownloadHistory, DownloadFiles
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
|
||||
class DownloadHistoryOper(DbOper):
|
||||
"""
|
||||
下载历史管理
|
||||
"""
|
||||
|
||||
def get_by_path(self, path: str) -> Optional[DownloadHistory]:
|
||||
"""
|
||||
按路径查询下载记录
|
||||
:param path: 数据key
|
||||
"""
|
||||
return DownloadHistory.get_by_path(self._db, path)
|
||||
|
||||
def get_by_hash(self, download_hash: str) -> Optional[DownloadHistory]:
|
||||
"""
|
||||
按Hash查询下载记录
|
||||
:param download_hash: 数据key
|
||||
"""
|
||||
return DownloadHistory.get_by_hash(self._db, download_hash)
|
||||
|
||||
def get_by_hashes(self, download_hashes: List[str]) -> Dict[str, DownloadHistory]:
|
||||
"""
|
||||
批量按 Hash 查询下载记录,并返回以 Hash 为键的映射。
|
||||
"""
|
||||
histories = DownloadHistory.get_by_hashes(self._db, download_hashes)
|
||||
return {
|
||||
history.download_hash: history
|
||||
for history in histories
|
||||
if history and history.download_hash
|
||||
}
|
||||
|
||||
def get_by_media_identity(
|
||||
self, media_source: MediaSource, media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
) -> List[DownloadHistory]:
|
||||
"""
|
||||
按规范媒体身份查询下载记录。
|
||||
:param media_source: 媒体数据源
|
||||
:param media_id: 数据源原生 ID
|
||||
:param music_type: 音乐实体类型
|
||||
"""
|
||||
return DownloadHistory.get_by_media_identity(
|
||||
self._db,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
music_type=music_type,
|
||||
)
|
||||
|
||||
def add(self, **kwargs):
|
||||
"""
|
||||
新增下载历史
|
||||
"""
|
||||
DownloadHistory(**kwargs).create(self._db)
|
||||
|
||||
def add_files(self, file_items: List[dict]):
|
||||
"""
|
||||
新增下载历史文件
|
||||
"""
|
||||
for file_item in file_items:
|
||||
downloadfile = DownloadFiles(**file_item)
|
||||
downloadfile.create(self._db)
|
||||
|
||||
def truncate_files(self):
|
||||
"""
|
||||
清空下载历史文件记录
|
||||
"""
|
||||
DownloadFiles.truncate(self._db)
|
||||
|
||||
def get_files_by_hash(self, download_hash: str, state: Optional[int] = None) -> List[DownloadFiles]:
|
||||
"""
|
||||
按Hash查询下载文件记录
|
||||
:param download_hash: 数据key
|
||||
:param state: 删除状态
|
||||
"""
|
||||
return DownloadFiles.get_by_hash(self._db, download_hash, state)
|
||||
|
||||
def get_file_by_fullpath(self, fullpath: str) -> Optional[DownloadFiles]:
|
||||
"""
|
||||
按fullpath查询下载文件记录
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
return cast(Optional[DownloadFiles],
|
||||
DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=False))
|
||||
|
||||
def get_files_by_fullpath(self, fullpath: str) -> List[DownloadFiles]:
|
||||
"""
|
||||
按fullpath查询下载文件记录
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
return cast(List[DownloadFiles],
|
||||
DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=True))
|
||||
|
||||
def get_files_by_savepath(self, fullpath: str) -> List[DownloadFiles]:
|
||||
"""
|
||||
按savepath查询下载文件记录
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
return DownloadFiles.get_by_savepath(self._db, fullpath)
|
||||
|
||||
def delete_file_by_fullpath(self, fullpath: str):
|
||||
"""
|
||||
按fullpath删除下载文件记录
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
DownloadFiles.delete_by_fullpath(self._db, fullpath)
|
||||
|
||||
def get_hash_by_fullpath(self, fullpath: str) -> Optional[str]:
|
||||
"""
|
||||
按fullpath查询下载文件记录hash
|
||||
:param fullpath: 数据key
|
||||
"""
|
||||
fileinfo = cast(Optional[DownloadFiles],
|
||||
DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=False))
|
||||
if fileinfo:
|
||||
return fileinfo.download_hash
|
||||
return ""
|
||||
|
||||
def list_by_page(self, page: int = 1, count: int = 30) -> List[DownloadHistory]:
|
||||
"""
|
||||
分页查询下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_page(self._db, page, count)
|
||||
|
||||
async def async_delete_history(self, historyid: int):
|
||||
"""
|
||||
异步删除下载记录。
|
||||
"""
|
||||
await DownloadHistory.async_delete(self._db, historyid)
|
||||
|
||||
def truncate(self):
|
||||
"""
|
||||
清空下载记录
|
||||
"""
|
||||
DownloadHistory.truncate(self._db)
|
||||
|
||||
def get_last_by(self, mtype=None, title: Optional[str] = None, year: Optional[str] = None,
|
||||
season: Optional[str] = None, episode: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None) -> List[DownloadHistory]:
|
||||
"""
|
||||
按类型、标题、年份、季集查询下载记录
|
||||
媒体身份 + mtype 或 title + year
|
||||
"""
|
||||
return DownloadHistory.get_last_by(db=self._db,
|
||||
mtype=mtype,
|
||||
title=title,
|
||||
year=year,
|
||||
season=season,
|
||||
episode=episode,
|
||||
media_source=media_source,
|
||||
media_id=media_id)
|
||||
|
||||
def list_by_user_date(self, date: str, username: Optional[str] = None) -> List[DownloadHistory]:
|
||||
"""
|
||||
查询某用户某时间之前的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_user_date(db=self._db,
|
||||
date=date,
|
||||
username=username)
|
||||
|
||||
def list_by_date(
|
||||
self, date: str, type: str, media_source: MediaSource, media_id: str,
|
||||
seasons: Optional[str] = None,
|
||||
) -> List[DownloadHistory]:
|
||||
"""
|
||||
查询某时间之后的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_date(db=self._db,
|
||||
date=date,
|
||||
type=type,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
seasons=seasons)
|
||||
|
||||
def list_by_type(self, mtype: str, days: int = 7) -> List[DownloadHistory]:
|
||||
"""
|
||||
获取指定类型的下载历史
|
||||
"""
|
||||
return DownloadHistory.list_by_type(db=self._db,
|
||||
mtype=mtype,
|
||||
days=days)
|
||||
|
||||
def delete_history(self, historyid):
|
||||
"""
|
||||
删除下载记录
|
||||
"""
|
||||
DownloadHistory.delete(self._db, historyid)
|
||||
|
||||
def delete_downloadfile(self, downloadfileid):
|
||||
"""
|
||||
删除下载文件记录
|
||||
"""
|
||||
DownloadFiles.delete(self._db, downloadfileid)
|
||||
@@ -0,0 +1,152 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.mediaserver import MediaServerItem
|
||||
|
||||
|
||||
class MediaServerOper(DbOper):
|
||||
"""
|
||||
媒体服务器数据管理
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[Session] = None):
|
||||
super().__init__(db)
|
||||
|
||||
@staticmethod
|
||||
def __prepare_payload(kwargs: dict) -> dict:
|
||||
"""
|
||||
过滤数据库模型不存在或不应由远端覆盖的字段
|
||||
"""
|
||||
return {
|
||||
k: v for k, v in kwargs.items()
|
||||
if hasattr(MediaServerItem, k) and k != "id"
|
||||
}
|
||||
|
||||
def add(self, **kwargs) -> bool:
|
||||
"""
|
||||
新增媒体服务器数据
|
||||
"""
|
||||
kwargs = self.__prepare_payload(kwargs)
|
||||
server = kwargs.get("server")
|
||||
item_id = kwargs.get("item_id")
|
||||
if not server or not item_id:
|
||||
return False
|
||||
item = MediaServerItem(**kwargs)
|
||||
if not item.get_by_server_itemid(self._db, server, item_id):
|
||||
item.create(self._db)
|
||||
return True
|
||||
return False
|
||||
|
||||
def upsert(self, **kwargs) -> bool:
|
||||
"""
|
||||
按媒体服务器和条目ID新增或更新数据
|
||||
"""
|
||||
kwargs = self.__prepare_payload(kwargs)
|
||||
server = kwargs.get("server")
|
||||
item_id = kwargs.get("item_id")
|
||||
if not server or not item_id:
|
||||
return False
|
||||
|
||||
item = MediaServerItem.get_by_server_itemid(self._db, server, item_id)
|
||||
if item:
|
||||
item.update(self._db, kwargs)
|
||||
return False
|
||||
|
||||
MediaServerItem(**kwargs).create(self._db)
|
||||
return True
|
||||
|
||||
def empty(self, server: Optional[str] = None):
|
||||
"""
|
||||
清空媒体服务器数据
|
||||
"""
|
||||
MediaServerItem.empty(self._db, server)
|
||||
|
||||
def delete_stale(self, server: str, sync_time: str) -> int:
|
||||
"""
|
||||
删除本轮同步未更新的旧数据
|
||||
"""
|
||||
return MediaServerItem.delete_stale(self._db, server, sync_time)
|
||||
|
||||
def delete_excluded_servers(self, servers: list[str]) -> int:
|
||||
"""
|
||||
删除未启用或已移除媒体服务器的数据
|
||||
"""
|
||||
return MediaServerItem.delete_excluded_servers(self._db, servers)
|
||||
|
||||
def exists(self, **kwargs) -> Optional[MediaServerItem]:
|
||||
"""
|
||||
判断媒体服务器数据是否存在
|
||||
"""
|
||||
if kwargs.get("media_source") and kwargs.get("media_id"):
|
||||
item = MediaServerItem.exist_by_media_identity(
|
||||
self._db,
|
||||
media_source=kwargs.get("media_source"),
|
||||
media_id=kwargs.get("media_id"),
|
||||
mtype=kwargs.get("mtype"),
|
||||
)
|
||||
elif kwargs.get("title"):
|
||||
# 按标题、类型、年份查
|
||||
item = MediaServerItem.exists_by_title(self._db, title=kwargs.get("title"),
|
||||
mtype=kwargs.get("mtype"), year=kwargs.get("year"))
|
||||
else:
|
||||
return None
|
||||
if not item:
|
||||
return None
|
||||
|
||||
if kwargs.get("season") is not None:
|
||||
# 判断季是否存在
|
||||
if not item.seasoninfo:
|
||||
return None
|
||||
seasoninfo = item.seasoninfo or {}
|
||||
if kwargs.get("season") not in seasoninfo.keys():
|
||||
return None
|
||||
return item
|
||||
|
||||
async def async_exists(self, **kwargs) -> Optional[MediaServerItem]:
|
||||
"""
|
||||
异步判断媒体服务器数据是否存在
|
||||
"""
|
||||
if kwargs.get("media_source") and kwargs.get("media_id"):
|
||||
item = await MediaServerItem.async_exist_by_media_identity(
|
||||
self._db,
|
||||
media_source=kwargs.get("media_source"),
|
||||
media_id=kwargs.get("media_id"),
|
||||
mtype=kwargs.get("mtype"),
|
||||
)
|
||||
elif kwargs.get("title"):
|
||||
# 按标题、类型、年份查
|
||||
item = await MediaServerItem.async_exists_by_title(self._db, title=kwargs.get("title"),
|
||||
mtype=kwargs.get("mtype"), year=kwargs.get("year"))
|
||||
else:
|
||||
return None
|
||||
if not item:
|
||||
return None
|
||||
|
||||
if kwargs.get("season") is not None:
|
||||
# 判断季是否存在
|
||||
if not item.seasoninfo:
|
||||
return None
|
||||
seasoninfo = item.seasoninfo or {}
|
||||
if kwargs.get("season") not in seasoninfo.keys():
|
||||
return None
|
||||
return item
|
||||
|
||||
def get_item_id(self, **kwargs) -> Optional[str]:
|
||||
"""
|
||||
获取媒体服务器数据ID
|
||||
"""
|
||||
item = self.exists(**kwargs)
|
||||
if not item:
|
||||
return None
|
||||
return str(item.item_id)
|
||||
|
||||
async def async_get_item_id(self, **kwargs) -> Optional[str]:
|
||||
"""
|
||||
异步获取媒体服务器数据ID
|
||||
"""
|
||||
item = await self.async_exists(**kwargs)
|
||||
if not item:
|
||||
return None
|
||||
return str(item.item_id)
|
||||
@@ -0,0 +1,143 @@
|
||||
import time
|
||||
from typing import Optional, Union
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.message import Message
|
||||
from app.schemas import MessageChannel, NotificationType
|
||||
|
||||
|
||||
class MessageOper(DbOper):
|
||||
"""
|
||||
消息数据管理
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
|
||||
super().__init__(db)
|
||||
|
||||
def add(self,
|
||||
channel: Optional[MessageChannel] = None,
|
||||
source: Optional[str] = None,
|
||||
mtype: Optional[NotificationType] = None,
|
||||
title: Optional[str] = None,
|
||||
text: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
link: Optional[str] = None,
|
||||
userid: Optional[str] = None,
|
||||
action: Optional[int] = 1,
|
||||
note: Optional[Union[list, dict]] = None,
|
||||
**kwargs) -> dict:
|
||||
"""
|
||||
新增消息
|
||||
:param channel: 消息渠道
|
||||
:param source: 来源
|
||||
:param mtype: 消息类型
|
||||
:param title: 标题
|
||||
:param text: 文本内容
|
||||
:param image: 图片
|
||||
:param link: 链接
|
||||
:param userid: 用户ID
|
||||
:param action: 消息方向:0-接收息,1-发送消息
|
||||
:param note: 附件json
|
||||
"""
|
||||
kwargs.update({
|
||||
"channel": channel.value if channel else '',
|
||||
"source": source,
|
||||
"mtype": mtype.value if mtype else '',
|
||||
"title": title,
|
||||
"text": text,
|
||||
"image": image,
|
||||
"link": link,
|
||||
"userid": userid,
|
||||
"action": action,
|
||||
"reg_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
"note": note or {}
|
||||
})
|
||||
|
||||
# 从kwargs中去掉Message中没有的字段
|
||||
for k in list(kwargs.keys()):
|
||||
if k not in Message.__table__.columns.keys(): # noqa
|
||||
kwargs.pop(k)
|
||||
|
||||
return Message(**kwargs).create_and_to_dict(self._db)
|
||||
|
||||
async def async_add(self,
|
||||
channel: Optional[MessageChannel] = None,
|
||||
source: Optional[str] = None,
|
||||
mtype: Optional[NotificationType] = None,
|
||||
title: Optional[str] = None,
|
||||
text: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
link: Optional[str] = None,
|
||||
userid: Optional[str] = None,
|
||||
action: Optional[int] = 1,
|
||||
note: Optional[Union[list, dict]] = None,
|
||||
**kwargs) -> Message:
|
||||
"""
|
||||
异步新增消息
|
||||
"""
|
||||
kwargs.update({
|
||||
"channel": channel.value if channel else '',
|
||||
"source": source,
|
||||
"mtype": mtype.value if mtype else '',
|
||||
"title": title,
|
||||
"text": text,
|
||||
"image": image,
|
||||
"link": link,
|
||||
"userid": userid,
|
||||
"action": action,
|
||||
"reg_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
"note": note or {}
|
||||
})
|
||||
|
||||
# 从kwargs中去掉Message中没有的字段
|
||||
for k in list(kwargs.keys()):
|
||||
if k not in Message.__table__.columns.keys(): # noqa
|
||||
kwargs.pop(k)
|
||||
|
||||
return await Message(**kwargs).async_create(self._db)
|
||||
|
||||
def list_by_page(self, page: int = 1, count: int = 30) -> list[Message]:
|
||||
"""
|
||||
分页获取消息记录。
|
||||
"""
|
||||
return Message.list_by_page(self._db, page, count)
|
||||
|
||||
def exists_by_source(self, source: str) -> bool:
|
||||
"""
|
||||
判断指定来源标识的消息记录是否存在。
|
||||
|
||||
:param source: 消息来源唯一标识
|
||||
:return: 是否存在匹配记录
|
||||
"""
|
||||
return Message.exists_by_source(self._db, source)
|
||||
|
||||
async def async_list_by_page(
|
||||
self, page: int = 1, count: int = 30
|
||||
) -> list[Message]:
|
||||
"""
|
||||
分页获取消息记录。
|
||||
"""
|
||||
return await Message.async_list_by_page(self._db, page, count)
|
||||
|
||||
async def async_list_sent_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
all_clear_before: Optional[str] = None,
|
||||
system_clear_before: Optional[str] = None,
|
||||
media_clear_before: Optional[str] = None,
|
||||
) -> list[Message]:
|
||||
"""
|
||||
分页获取系统发送的通知消息。
|
||||
"""
|
||||
return await Message.async_list_sent_by_page(
|
||||
self._db,
|
||||
page,
|
||||
count,
|
||||
all_clear_before=all_clear_before,
|
||||
system_clear_before=system_clear_before,
|
||||
media_clear_before=media_clear_before,
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.plugindata import PluginData
|
||||
|
||||
|
||||
class PluginDataOper(DbOper):
|
||||
"""
|
||||
插件数据管理
|
||||
"""
|
||||
|
||||
def save(self, plugin_id: str, key: str, value: Any):
|
||||
"""
|
||||
保存插件数据
|
||||
:param plugin_id: 插件id
|
||||
:param key: 数据key
|
||||
:param value: 数据值
|
||||
"""
|
||||
plugin = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
|
||||
if plugin:
|
||||
plugin.update(self._db, {
|
||||
"value": value
|
||||
})
|
||||
else:
|
||||
PluginData(plugin_id=plugin_id, key=key, value=value).create(self._db)
|
||||
|
||||
async def async_save(self, plugin_id: str, key: str, value: Any) -> None:
|
||||
"""
|
||||
异步保存插件数据
|
||||
|
||||
:param plugin_id: 插件ID
|
||||
:param key: 数据键
|
||||
:param value: 数据值
|
||||
"""
|
||||
plugin = await PluginData.async_get_plugin_data_by_key(
|
||||
self._db, plugin_id, key
|
||||
)
|
||||
if plugin:
|
||||
await plugin.async_update(self._db, {"value": value})
|
||||
else:
|
||||
await PluginData(
|
||||
plugin_id=plugin_id, key=key, value=value
|
||||
).async_create(self._db)
|
||||
|
||||
def get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
|
||||
"""
|
||||
获取插件数据
|
||||
:param plugin_id: 插件id
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
data = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
|
||||
if not data:
|
||||
return None
|
||||
return data.value
|
||||
else:
|
||||
return PluginData.get_plugin_data(self._db, plugin_id)
|
||||
|
||||
async def async_get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
|
||||
"""
|
||||
异步获取插件数据。
|
||||
:param plugin_id: 插件id
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
data = await PluginData.async_get_plugin_data_by_key(
|
||||
self._db, plugin_id, key
|
||||
)
|
||||
if not data:
|
||||
return None
|
||||
return data.value
|
||||
return await PluginData.async_get_plugin_data(self._db, plugin_id)
|
||||
|
||||
def del_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
|
||||
"""
|
||||
删除插件数据
|
||||
:param plugin_id: 插件id
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
PluginData.del_plugin_data_by_key(self._db, plugin_id, key)
|
||||
else:
|
||||
PluginData.del_plugin_data(self._db, plugin_id)
|
||||
|
||||
def truncate(self):
|
||||
"""
|
||||
清空插件数据
|
||||
"""
|
||||
PluginData.truncate(self._db)
|
||||
|
||||
def get_data_all(self, plugin_id: str) -> Any:
|
||||
"""
|
||||
获取插件所有数据
|
||||
:param plugin_id: 插件id
|
||||
"""
|
||||
return PluginData.get_plugin_data_by_plugin_id(self._db, plugin_id)
|
||||
|
||||
async def async_get_data_all(self, plugin_id: str) -> Any:
|
||||
"""
|
||||
异步获取插件所有数据。
|
||||
:param plugin_id: 插件id
|
||||
"""
|
||||
return await PluginData.async_get_plugin_data_by_plugin_id(self._db, plugin_id)
|
||||
@@ -0,0 +1,349 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models import SiteIcon
|
||||
from app.db.models.site import Site
|
||||
from app.db.models.sitestatistic import SiteStatistic
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
|
||||
|
||||
class SiteOper(DbOper):
|
||||
"""
|
||||
站点管理
|
||||
"""
|
||||
|
||||
def add(self, **kwargs) -> Tuple[bool, str]:
|
||||
"""
|
||||
新增站点
|
||||
"""
|
||||
site = Site(**kwargs)
|
||||
if not site.get_by_domain(self._db, kwargs.get("domain")):
|
||||
site.create(self._db)
|
||||
return True, "新增站点成功"
|
||||
return False, "站点已存在"
|
||||
|
||||
def get(self, sid: int) -> Optional[Site]:
|
||||
"""
|
||||
查询单个站点
|
||||
"""
|
||||
return Site.get(self._db, sid)
|
||||
|
||||
async def async_get(self, sid: int) -> Optional[Site]:
|
||||
"""
|
||||
异步查询单个站点
|
||||
"""
|
||||
return await Site.async_get(self._db, sid)
|
||||
|
||||
def list(self) -> List[Site]:
|
||||
"""
|
||||
获取站点列表
|
||||
"""
|
||||
return Site.list(self._db)
|
||||
|
||||
async def async_list(self) -> List[Site]:
|
||||
"""
|
||||
异步获取站点列表
|
||||
"""
|
||||
return await Site.async_list(self._db)
|
||||
|
||||
def list_order_by_pri(self) -> List[Site]:
|
||||
"""
|
||||
获取站点列表
|
||||
"""
|
||||
return Site.list_order_by_pri(self._db)
|
||||
|
||||
def list_active(self) -> List[Site]:
|
||||
"""
|
||||
按状态获取站点列表
|
||||
"""
|
||||
return Site.get_actives(self._db)
|
||||
|
||||
async def async_list_active(self) -> List[Site]:
|
||||
"""
|
||||
异步按状态获取站点列表
|
||||
"""
|
||||
return await Site.async_get_actives(self._db)
|
||||
|
||||
def delete(self, sid: int):
|
||||
"""
|
||||
删除站点
|
||||
"""
|
||||
Site.delete(self._db, sid)
|
||||
|
||||
def update(self, sid: int, payload: dict) -> Optional[Site]:
|
||||
"""
|
||||
更新站点
|
||||
"""
|
||||
site = Site.get(self._db, sid)
|
||||
if not site:
|
||||
return None
|
||||
site.update(self._db, payload)
|
||||
return site
|
||||
|
||||
async def async_update(self, sid: int, payload: dict) -> Optional[Site]:
|
||||
"""
|
||||
异步更新站点。
|
||||
"""
|
||||
site = await self.async_get(sid)
|
||||
if site:
|
||||
await site.async_update(self._db, payload)
|
||||
return site
|
||||
|
||||
def get_by_domain(self, domain: str) -> Optional[Site]:
|
||||
"""
|
||||
按域名获取站点
|
||||
"""
|
||||
return Site.get_by_domain(self._db, domain)
|
||||
|
||||
async def async_get_by_domain(self, domain: str) -> Optional[Site]:
|
||||
"""
|
||||
异步按域名获取站点
|
||||
"""
|
||||
return await Site.async_get_by_domain(self._db, domain)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Optional[Site]:
|
||||
"""
|
||||
异步按名称获取站点
|
||||
"""
|
||||
return await Site.async_get_by_name(self._db, name)
|
||||
|
||||
def get_domains_by_ids(self, ids: List[int]) -> List[Optional[str]]:
|
||||
"""
|
||||
按ID获取站点域名
|
||||
"""
|
||||
return Site.get_domains_by_ids(self._db, ids)
|
||||
|
||||
def exists(self, domain: str) -> bool:
|
||||
"""
|
||||
判断站点是否存在
|
||||
"""
|
||||
return Site.get_by_domain(self._db, domain) is not None
|
||||
|
||||
def update_cookie(self, domain: str, cookies: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
更新站点Cookie
|
||||
"""
|
||||
site = Site.get_by_domain(self._db, domain)
|
||||
if not site:
|
||||
return False, "站点不存在"
|
||||
site.update(self._db, {
|
||||
"cookie": cookies
|
||||
})
|
||||
return True, "更新站点Cookie成功"
|
||||
|
||||
def update_rss(self, domain: str, rss: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
更新站点rss
|
||||
"""
|
||||
site = Site.get_by_domain(self._db, domain)
|
||||
if not site:
|
||||
return False, "站点不存在"
|
||||
site.update(self._db, {
|
||||
"rss": rss
|
||||
})
|
||||
return True, "更新站点RSS地址成功"
|
||||
|
||||
def update_userdata(self, domain: str, name: str, payload: dict) -> Tuple[bool, str]:
|
||||
"""
|
||||
更新站点用户数据
|
||||
"""
|
||||
# 当前系统日期
|
||||
current_day = datetime.now().strftime('%Y-%m-%d')
|
||||
current_time = datetime.now().strftime('%H:%M:%S')
|
||||
payload.update({
|
||||
"domain": domain,
|
||||
"name": name,
|
||||
"updated_day": current_day,
|
||||
"updated_time": current_time,
|
||||
"err_msg": payload.get("err_msg") or ""
|
||||
})
|
||||
# 按站点+天判断是否存在数据
|
||||
siteuserdatas = SiteUserData.get_by_domain(self._db, domain=domain, workdate=current_day)
|
||||
if siteuserdatas:
|
||||
# 存在则更新
|
||||
if not payload.get("err_msg"):
|
||||
siteuserdatas[0].update(self._db, payload)
|
||||
else:
|
||||
# 不存在则插入
|
||||
SiteUserData(**payload).create(self._db)
|
||||
return True, "更新站点用户数据成功"
|
||||
|
||||
def get_userdata(self) -> List[SiteUserData]:
|
||||
"""
|
||||
获取站点用户数据
|
||||
"""
|
||||
return SiteUserData.list(self._db)
|
||||
|
||||
def get_userdata_by_domain(self, domain: str, workdate: Optional[str] = None) -> List[SiteUserData]:
|
||||
"""
|
||||
获取站点用户数据
|
||||
"""
|
||||
return SiteUserData.get_by_domain(self._db, domain=domain, workdate=workdate)
|
||||
|
||||
async def async_get_userdata_by_domain(
|
||||
self, domain: str, workdate: Optional[str] = None
|
||||
) -> List[SiteUserData]:
|
||||
"""
|
||||
异步获取站点用户数据。
|
||||
"""
|
||||
return await SiteUserData.async_get_by_domain(
|
||||
self._db, domain=domain, workdate=workdate
|
||||
)
|
||||
|
||||
def get_userdata_by_date(self, date: str) -> List[SiteUserData]:
|
||||
"""
|
||||
获取站点用户数据
|
||||
"""
|
||||
return SiteUserData.get_by_date(self._db, date)
|
||||
|
||||
def get_userdata_latest(self) -> List[SiteUserData]:
|
||||
"""
|
||||
获取站点最新数据
|
||||
"""
|
||||
return SiteUserData.get_latest(self._db)
|
||||
|
||||
def get_icon_by_domain(self, domain: str) -> Optional[SiteIcon]:
|
||||
"""
|
||||
按域名获取站点图标
|
||||
"""
|
||||
return SiteIcon.get_by_domain(self._db, domain)
|
||||
|
||||
def update_icon(self, name: str, domain: str, icon_url: str, icon_base64: str) -> bool:
|
||||
"""
|
||||
更新站点图标
|
||||
"""
|
||||
icon_base64 = f"data:image/ico;base64,{icon_base64}" if icon_base64 else ""
|
||||
siteicon = self.get_icon_by_domain(domain)
|
||||
if not siteicon:
|
||||
SiteIcon(name=name, domain=domain, url=icon_url, base64=icon_base64).create(self._db)
|
||||
elif icon_base64:
|
||||
siteicon.update(self._db, {
|
||||
"url": icon_url,
|
||||
"base64": icon_base64
|
||||
})
|
||||
return True
|
||||
|
||||
def success(self, domain: str, seconds: Optional[int] = None):
|
||||
"""
|
||||
站点访问成功
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
# 使用深复制确保 note 是全新的字典对象
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
|
||||
avg_seconds = sum([v for v in note.values()]) // avg_times
|
||||
|
||||
sta.update(self._db, {
|
||||
"success": sta.success + 1,
|
||||
"seconds": avg_seconds or sta.seconds,
|
||||
"lst_state": 0,
|
||||
"lst_mod_date": lst_date,
|
||||
"note": note
|
||||
})
|
||||
else:
|
||||
note = {}
|
||||
if seconds is not None:
|
||||
note = {
|
||||
lst_date: seconds or 1
|
||||
}
|
||||
SiteStatistic(
|
||||
domain=domain,
|
||||
success=1,
|
||||
fail=0,
|
||||
seconds=seconds or 1,
|
||||
lst_state=0,
|
||||
lst_mod_date=lst_date,
|
||||
note=note
|
||||
).create(self._db)
|
||||
|
||||
def fail(self, domain: str):
|
||||
"""
|
||||
站点访问失败
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = SiteStatistic.get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
sta.update(self._db, {
|
||||
"fail": sta.fail + 1,
|
||||
"lst_state": 1,
|
||||
"lst_mod_date": lst_date
|
||||
})
|
||||
else:
|
||||
SiteStatistic(
|
||||
domain=domain,
|
||||
success=0,
|
||||
fail=1,
|
||||
lst_state=1,
|
||||
lst_mod_date=lst_date
|
||||
).create(self._db)
|
||||
|
||||
async def async_success(self, domain: str, seconds: Optional[int] = None):
|
||||
"""
|
||||
异步站点访问成功
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
# 使用深复制确保 note 是全新的字典对象
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
|
||||
avg_seconds = sum([v for v in note.values()]) // avg_times
|
||||
|
||||
await sta.async_update(self._db, {
|
||||
"success": sta.success + 1,
|
||||
"seconds": avg_seconds or sta.seconds,
|
||||
"lst_state": 0,
|
||||
"lst_mod_date": lst_date,
|
||||
"note": note
|
||||
})
|
||||
else:
|
||||
note = {}
|
||||
if seconds is not None:
|
||||
note = {
|
||||
lst_date: seconds or 1
|
||||
}
|
||||
await SiteStatistic(
|
||||
domain=domain,
|
||||
success=1,
|
||||
fail=0,
|
||||
seconds=seconds or 1,
|
||||
lst_state=0,
|
||||
lst_mod_date=lst_date,
|
||||
note=note
|
||||
).async_create(self._db)
|
||||
|
||||
async def async_fail(self, domain: str):
|
||||
"""
|
||||
异步站点访问失败
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
await sta.async_update(self._db, {
|
||||
"fail": sta.fail + 1,
|
||||
"lst_state": 1,
|
||||
"lst_mod_date": lst_date
|
||||
})
|
||||
else:
|
||||
await SiteStatistic(
|
||||
domain=domain,
|
||||
success=0,
|
||||
fail=1,
|
||||
lst_state=1,
|
||||
lst_mod_date=lst_date
|
||||
).async_create(self._db)
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
订阅数据访问。
|
||||
|
||||
本模块只收敛针对订阅表的读写。把 MediaInfo / MusicInfo 翻译成一行订阅是订阅业务的
|
||||
规则,住在 app/application/subscribe.py;这里收到的 payload 已经是纯粹的持久化字段,
|
||||
因此不 import 任何领域对象。
|
||||
|
||||
留在这一层的只有列类型强转与建库时间戳——它们跟着订阅表的列走,换谁来调都一样。
|
||||
"""
|
||||
import time
|
||||
from typing import Any, Tuple, List, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.subscribe import Subscribe
|
||||
from app.db.models.subscribehistory import SubscribeHistory
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
INTEGER_FLAG_FIELDS = ("best_version", "best_version_full", "search_imdbid", "manual_total_episode")
|
||||
|
||||
|
||||
def _normalize_integer_flags(payload: dict, fields: Tuple[str, ...] = INTEGER_FLAG_FIELDS) -> dict:
|
||||
"""
|
||||
将历史兼容的布尔开关转换为整型值,避免 PostgreSQL 严格类型检查失败。
|
||||
"""
|
||||
normalized_payload = dict(payload)
|
||||
for field in fields:
|
||||
if isinstance(normalized_payload.get(field), bool):
|
||||
normalized_payload[field] = int(normalized_payload[field])
|
||||
return normalized_payload
|
||||
|
||||
|
||||
def _normalize_year(year: Optional[int | str]) -> Optional[str]:
|
||||
"""
|
||||
订阅表的 year 列为字符串类型,而识别链路的媒体年份可能是数字
|
||||
(音乐等来源),写库前统一转换为字符串避免数据库类型错误。
|
||||
"""
|
||||
if year is None:
|
||||
return None
|
||||
return str(year)
|
||||
|
||||
|
||||
def _persistable(payload: dict) -> dict:
|
||||
"""
|
||||
把应用层给的写入字段落成订阅表能收的一行。
|
||||
|
||||
做两件事。一是列类型强转:PostgreSQL 的整型列拒收布尔值、字符串列拒收数字,而
|
||||
SQLite 会靠类型亲和悄悄替我们转好——漏了只在生产库上炸,所以放在紧挨建模的地方。
|
||||
二是盖建库时间戳:调用方传进来的 date 不作数,否则订阅列表的默认排序与过期清理
|
||||
都会读到一个假的建库时间。
|
||||
:param payload: 应用层翻译好的写入字段
|
||||
:return: 可直接建模的字段字典
|
||||
"""
|
||||
persistable = _normalize_integer_flags(payload)
|
||||
persistable["year"] = _normalize_year(persistable.get("year"))
|
||||
# search_imdbid 参与搜索分支判定,None 与真值都要归一到 0/1,否则同一列在不同
|
||||
# 订阅上会存出三种形态,PG 上还会直接拒写
|
||||
persistable["search_imdbid"] = 1 if persistable.get("search_imdbid") else 0
|
||||
persistable["date"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
return persistable
|
||||
|
||||
|
||||
class SubscribeOper(DbOper):
|
||||
"""
|
||||
订阅管理
|
||||
"""
|
||||
|
||||
def _exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
|
||||
"""
|
||||
按身份查重。
|
||||
:param identity: 查重身份
|
||||
:param username: 非空时只在该用户的订阅内查
|
||||
:return: 命中的订阅行,未命中为 None
|
||||
"""
|
||||
if username:
|
||||
return Subscribe.exists_by_username(self._db, username=username, **identity)
|
||||
return Subscribe.exists(self._db, **identity)
|
||||
|
||||
async def _async_exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
|
||||
"""
|
||||
按身份查重(异步)。
|
||||
:param identity: 查重身份
|
||||
:param username: 非空时只在该用户的订阅内查
|
||||
:return: 命中的订阅行,未命中为 None
|
||||
"""
|
||||
if username:
|
||||
return await Subscribe.async_exists_by_username(self._db, username=username, **identity)
|
||||
return await Subscribe.async_exists(self._db, **identity)
|
||||
|
||||
def add(self, identity: dict, payload: dict,
|
||||
username: Optional[str] = None) -> Tuple[int, str]:
|
||||
"""
|
||||
新增订阅:命中既有订阅则原样返回,否则落库后回读。
|
||||
|
||||
回读不是多余的一次查询——写入可能被唯一约束或事务回滚吞掉,此时若报成功,
|
||||
调用方会继续按订阅已建立往下走,用户看到「订阅成功」却永远等不到资源。
|
||||
:param identity: 查重身份(media_source/media_id/music_type/season/episode_group)
|
||||
:param payload: 订阅表的写入字段,媒体翻译由 app/application/subscribe.py 完成
|
||||
:param username: 非空时把查重限定在该用户的订阅内
|
||||
:return: (订阅 ID, 结果说明);ID 为 0 表示未新增
|
||||
"""
|
||||
subscribe = self._exists(identity, username)
|
||||
if subscribe:
|
||||
return subscribe.id, "订阅已存在"
|
||||
Subscribe(**_persistable(payload)).create(self._db)
|
||||
subscribe = self._exists(identity, username)
|
||||
if not subscribe:
|
||||
return 0, "新增订阅失败"
|
||||
return subscribe.id, "新增订阅成功"
|
||||
|
||||
async def async_add(self, identity: dict, payload: dict,
|
||||
username: Optional[str] = None) -> Tuple[int, str]:
|
||||
"""
|
||||
异步新增订阅,语义与 add 完全一致。
|
||||
:param identity: 查重身份(media_source/media_id/music_type/season/episode_group)
|
||||
:param payload: 订阅表的写入字段,媒体翻译由 app/application/subscribe.py 完成
|
||||
:param username: 非空时把查重限定在该用户的订阅内
|
||||
:return: (订阅 ID, 结果说明);ID 为 0 表示未新增
|
||||
"""
|
||||
subscribe = await self._async_exists(identity, username)
|
||||
if subscribe:
|
||||
return subscribe.id, "订阅已存在"
|
||||
await Subscribe(**_persistable(payload)).async_create(self._db)
|
||||
subscribe = await self._async_exists(identity, username)
|
||||
if not subscribe:
|
||||
return 0, "新增订阅失败"
|
||||
return subscribe.id, "新增订阅成功"
|
||||
|
||||
def exists(
|
||||
self, media_source: MediaSource, media_id: str,
|
||||
season: Optional[int] = None, episode_group: Optional[str] = None,
|
||||
music_type: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
按媒体身份、季号及可选剧集组判断订阅是否存在。
|
||||
"""
|
||||
identity_params = {
|
||||
"media_source": media_source,
|
||||
"media_id": media_id,
|
||||
"music_type": music_type,
|
||||
"season": season,
|
||||
"episode_group": episode_group,
|
||||
}
|
||||
return bool(Subscribe.exists(self._db, **identity_params))
|
||||
|
||||
def get(self, sid: int) -> Optional[Subscribe]:
|
||||
"""
|
||||
获取订阅
|
||||
"""
|
||||
return Subscribe.get(self._db, rid=sid)
|
||||
|
||||
async def async_get(self, sid: int) -> Optional[Subscribe]:
|
||||
"""
|
||||
获取订阅
|
||||
"""
|
||||
return await Subscribe.async_get(self._db, rid=sid)
|
||||
|
||||
def get_by(
|
||||
self, type: str, media_source: MediaSource, media_id: str,
|
||||
season: Optional[str] = None,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[Subscribe]:
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
return Subscribe.get_by(
|
||||
self._db, type, media_source, media_id, season, music_type,
|
||||
)
|
||||
|
||||
async def async_get_by(
|
||||
self, type: str, media_source: MediaSource, media_id: str,
|
||||
season: Optional[str] = None,
|
||||
music_type: Optional[str] = None,
|
||||
) -> Optional[Subscribe]:
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
return await Subscribe.async_get_by(
|
||||
self._db, type, media_source, media_id, season, music_type,
|
||||
)
|
||||
|
||||
def list(self, state: Optional[str] = None) -> List[Subscribe]:
|
||||
"""
|
||||
获取订阅列表
|
||||
"""
|
||||
if state:
|
||||
return Subscribe.get_by_state(self._db, state)
|
||||
return Subscribe.list(self._db)
|
||||
|
||||
async def async_list(self, state: Optional[str] = None) -> List[Subscribe]:
|
||||
"""
|
||||
异步获取订阅列表
|
||||
"""
|
||||
if state:
|
||||
return await Subscribe.async_get_by_state(self._db, state)
|
||||
return await Subscribe.async_list(self._db)
|
||||
|
||||
def delete(self, sid: int):
|
||||
"""
|
||||
删除订阅
|
||||
"""
|
||||
Subscribe.delete(self._db, rid=sid)
|
||||
|
||||
async def async_delete(self, sid: int):
|
||||
"""
|
||||
异步删除订阅。
|
||||
"""
|
||||
await Subscribe.async_delete(self._db, rid=sid)
|
||||
|
||||
async def async_update(self, sid: int, payload: dict) -> Optional[Subscribe]:
|
||||
"""
|
||||
异步更新订阅。
|
||||
"""
|
||||
subscribe = await self.async_get(sid)
|
||||
if subscribe:
|
||||
payload = _normalize_integer_flags(payload)
|
||||
await subscribe.async_update(self._db, payload)
|
||||
return subscribe
|
||||
|
||||
async def async_update_filter_groups(
|
||||
self, sid: int, filter_groups: List[str]
|
||||
) -> Optional[Subscribe]:
|
||||
"""
|
||||
异步更新订阅使用的过滤规则组。
|
||||
"""
|
||||
return await self.async_update(sid, {"filter_groups": filter_groups})
|
||||
|
||||
def update(self, sid: int, payload: dict) -> Optional[Subscribe]:
|
||||
"""
|
||||
更新订阅
|
||||
"""
|
||||
subscribe = self.get(sid)
|
||||
if subscribe:
|
||||
payload = _normalize_integer_flags(payload)
|
||||
subscribe.update(self._db, payload)
|
||||
return subscribe
|
||||
|
||||
def list_by_username(self, username: str, state: Optional[str] = None,
|
||||
mtype: Optional[str] = None) -> List[Subscribe]:
|
||||
"""
|
||||
获取指定用户的订阅
|
||||
"""
|
||||
return Subscribe.list_by_username(self._db, username=username, state=state, mtype=mtype)
|
||||
|
||||
def list_by_type(self, mtype: str, days: int = 7) -> List[Subscribe]:
|
||||
"""
|
||||
获取指定类型的订阅
|
||||
"""
|
||||
return Subscribe.list_by_type(self._db, mtype=mtype, days=days)
|
||||
|
||||
def add_history(self, **kwargs):
|
||||
"""
|
||||
新增订阅
|
||||
"""
|
||||
# 去除kwargs中 SubscribeHistory 没有的字段
|
||||
kwargs = {k: v for k, v in kwargs.items() if hasattr(SubscribeHistory, k)}
|
||||
kwargs = _normalize_integer_flags(kwargs)
|
||||
# 更新完成订阅时间
|
||||
kwargs.update({"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())})
|
||||
# 去掉主键
|
||||
if "id" in kwargs:
|
||||
kwargs.pop("id")
|
||||
subscribe = SubscribeHistory(**kwargs)
|
||||
subscribe.create(self._db)
|
||||
|
||||
def exist_history(
|
||||
self, media_source: MediaSource, media_id: str,
|
||||
season: Optional[int] = None, episode_group: Optional[str] = None,
|
||||
music_type: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
按媒体身份、季号及可选剧集组判断订阅历史是否存在。
|
||||
"""
|
||||
identity_params = {
|
||||
"media_source": media_source,
|
||||
"media_id": media_id,
|
||||
"music_type": music_type,
|
||||
"season": season,
|
||||
"episode_group": episode_group,
|
||||
}
|
||||
return bool(SubscribeHistory.exists(self._db, **identity_params))
|
||||
@@ -0,0 +1,26 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.subscribehistory import SubscribeHistory
|
||||
|
||||
|
||||
class SubscribeHistoryOper(DbOper):
|
||||
"""
|
||||
订阅历史管理。
|
||||
"""
|
||||
|
||||
async def async_list_by_type(
|
||||
self,
|
||||
mtype: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
) -> List[SubscribeHistory]:
|
||||
"""
|
||||
异步按媒体类型分页查询订阅历史。
|
||||
"""
|
||||
return await SubscribeHistory.async_list_by_type(
|
||||
self._db,
|
||||
mtype=mtype,
|
||||
page=page,
|
||||
count=count,
|
||||
)
|
||||
@@ -0,0 +1,136 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import threading
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.systemconfig import SystemConfig
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
class SystemConfigOper(DbOper, metaclass=Singleton):
|
||||
"""
|
||||
系统配置管理
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
加载配置到内存
|
||||
"""
|
||||
super().__init__()
|
||||
self.__SYSTEMCONF = {}
|
||||
self._rlock = threading.RLock()
|
||||
self._alock = asyncio.Lock()
|
||||
for item in SystemConfig.list(self._db):
|
||||
self.__SYSTEMCONF[item.key] = item.value
|
||||
|
||||
def set(self, key: Union[str, SystemConfigKey], value: Any) -> Optional[bool]:
|
||||
"""
|
||||
设置系统设置
|
||||
:param key: 配置键
|
||||
:param value: 配置值
|
||||
:return: 是否设置成功(True 成功/False 失败/None 无需更新)
|
||||
"""
|
||||
if isinstance(key, SystemConfigKey):
|
||||
key = key.value
|
||||
with self._rlock:
|
||||
# 旧值
|
||||
old_value = self.__SYSTEMCONF.get(key)
|
||||
# 更新内存(deepcopy避免内存共享)
|
||||
self.__SYSTEMCONF[key] = copy.deepcopy(value)
|
||||
conf = SystemConfig.get_by_key(self._db, key)
|
||||
if conf:
|
||||
if old_value != value:
|
||||
# 假值(False/0/None/空容器)同样落库而不是删除记录:
|
||||
# 读取端以「无记录」表示未配置并回落默认值,删除会使布尔开关的关闭态无法持久化
|
||||
conf.update(self._db, {"value": value})
|
||||
return True
|
||||
return None
|
||||
else:
|
||||
conf = SystemConfig(key=key, value=value)
|
||||
conf.create(self._db)
|
||||
return True
|
||||
|
||||
async def async_set(self, key: Union[str, SystemConfigKey], value: Any) -> Optional[bool]:
|
||||
"""
|
||||
异步设置系统设置
|
||||
:param key: 配置键
|
||||
:param value: 配置值
|
||||
:return: 是否设置成功(True 成功/False 失败/None 无需更新)
|
||||
"""
|
||||
if isinstance(key, SystemConfigKey):
|
||||
key = key.value
|
||||
async with self._alock:
|
||||
conf = await SystemConfig.async_get_by_key(self._db, key)
|
||||
# 确定是否需要更新数据库
|
||||
needs_db_update = False
|
||||
if conf:
|
||||
if conf.value != value:
|
||||
needs_db_update = True
|
||||
else: # 记录不存在,总是需要创建/更新
|
||||
needs_db_update = True
|
||||
if not needs_db_update:
|
||||
# 即使数据库值相同,也要确保缓存同步
|
||||
with self._rlock:
|
||||
self.__SYSTEMCONF[key] = copy.deepcopy(value)
|
||||
return None
|
||||
# 执行数据库更新
|
||||
if conf:
|
||||
# 假值(False/0/None/空容器)同样落库而不是删除记录:
|
||||
# 读取端以「无记录」表示未配置并回落默认值,删除会使布尔开关的关闭态无法持久化
|
||||
await conf.async_update(self._db, {"value": value})
|
||||
else:
|
||||
conf = SystemConfig(key=key, value=value)
|
||||
await conf.async_create(self._db)
|
||||
# 数据库更新成功后,再更新缓存
|
||||
with self._rlock:
|
||||
self.__SYSTEMCONF[key] = copy.deepcopy(value)
|
||||
return True
|
||||
|
||||
def get(self, key: Optional[Union[str, SystemConfigKey]] = None) -> Any:
|
||||
"""
|
||||
获取系统设置
|
||||
"""
|
||||
if isinstance(key, SystemConfigKey):
|
||||
key = key.value
|
||||
if not key:
|
||||
return self.all()
|
||||
with self._rlock:
|
||||
# 避免将__SYSTEMCONF内的值引用出去,会导致set时误判没有变动
|
||||
return copy.deepcopy(self.__SYSTEMCONF.get(key))
|
||||
|
||||
def increment(self, key: SystemConfigKey, step: int = 1) -> int:
|
||||
"""
|
||||
原子递增整数系统设置
|
||||
|
||||
:param key: 配置键
|
||||
:param step: 递增步长
|
||||
:return: 递增后的整数值
|
||||
"""
|
||||
with self._rlock:
|
||||
value = int(self.get(key) or 0) + step
|
||||
self.set(key, value)
|
||||
return value
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
获取所有系统设置
|
||||
"""
|
||||
with self._rlock:
|
||||
# 避免将__SYSTEMCONF内的值引用出去,会导致set时误判没有变动
|
||||
return copy.deepcopy(self.__SYSTEMCONF)
|
||||
|
||||
def delete(self, key: Union[str, SystemConfigKey]) -> bool:
|
||||
"""
|
||||
删除系统设置
|
||||
"""
|
||||
if isinstance(key, SystemConfigKey):
|
||||
key = key.value
|
||||
with self._rlock:
|
||||
# 更新内存
|
||||
self.__SYSTEMCONF.pop(key, None)
|
||||
# 写入数据库
|
||||
conf = SystemConfig.get_by_key(self._db, key)
|
||||
if conf:
|
||||
conf.delete(self._db, conf.id)
|
||||
return True
|
||||
@@ -0,0 +1,254 @@
|
||||
import time
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
|
||||
class TransferHistoryOper(DbOper):
|
||||
"""
|
||||
转移历史管理
|
||||
"""
|
||||
|
||||
def get(self, historyid: int) -> Optional[TransferHistory]:
|
||||
"""
|
||||
获取转移历史
|
||||
:param historyid: 转移历史id
|
||||
"""
|
||||
return TransferHistory.get(self._db, historyid)
|
||||
|
||||
async def async_get(self, historyid: int) -> Optional[TransferHistory]:
|
||||
"""
|
||||
异步获取转移历史。
|
||||
"""
|
||||
return await TransferHistory.async_get(self._db, historyid)
|
||||
|
||||
async def async_list_by_title(
|
||||
self,
|
||||
title: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: Optional[bool] = None,
|
||||
) -> List[TransferHistory]:
|
||||
"""
|
||||
异步按标题分页查询转移记录。
|
||||
"""
|
||||
return await TransferHistory.async_list_by_title(
|
||||
self._db, title=title, page=page, count=count, status=status
|
||||
)
|
||||
|
||||
async def async_list_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: Optional[bool] = None,
|
||||
) -> List[TransferHistory]:
|
||||
"""
|
||||
异步分页查询转移记录。
|
||||
"""
|
||||
return await TransferHistory.async_list_by_page(
|
||||
self._db, page=page, count=count, status=status
|
||||
)
|
||||
|
||||
async def async_count(self, status: Optional[bool] = None) -> Optional[int]:
|
||||
"""
|
||||
异步统计转移记录数量。
|
||||
"""
|
||||
return await TransferHistory.async_count(self._db, status=status)
|
||||
|
||||
async def async_count_by_title(
|
||||
self,
|
||||
title: str,
|
||||
status: Optional[bool] = None,
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
异步按标题统计转移记录数量。
|
||||
"""
|
||||
return await TransferHistory.async_count_by_title(
|
||||
self._db, title=title, status=status
|
||||
)
|
||||
|
||||
def get_by_title(self, title: str) -> List[TransferHistory]:
|
||||
"""
|
||||
按标题查询转移记录
|
||||
:param title: 数据key
|
||||
"""
|
||||
return TransferHistory.list_by_title(self._db, title)
|
||||
|
||||
def get_by_src(
|
||||
self, src: str, storage: Optional[str] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
"""
|
||||
按源查询转移记录
|
||||
:param src: 数据key
|
||||
:param storage: 存储类型
|
||||
:return: 命中的整理记录,未命中时返回 None
|
||||
"""
|
||||
return TransferHistory.get_by_src(self._db, src, storage)
|
||||
|
||||
def get_success_by_src(
|
||||
self, src: str, storage: Optional[str] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
"""
|
||||
按源查询成功的转移记录,源路径原样精确匹配
|
||||
:param src: 数据key
|
||||
:param storage: 存储类型
|
||||
:return: 命中的成功整理记录,未命中时返回 None
|
||||
"""
|
||||
return TransferHistory.get_success_by_src(self._db, src, storage)
|
||||
|
||||
def get_by_dest(
|
||||
self, dest: str, storage: Optional[str] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
"""
|
||||
按转移路径查询转移记录
|
||||
:param dest: 数据key
|
||||
:param storage: 存储类型
|
||||
"""
|
||||
return TransferHistory.get_by_dest(self._db, dest, storage)
|
||||
|
||||
def list_success_by_src(
|
||||
self,
|
||||
src: str,
|
||||
storage: Optional[str] = None,
|
||||
recursive: bool = False,
|
||||
) -> List[TransferHistory]:
|
||||
"""
|
||||
按源路径查询成功整理记录。
|
||||
|
||||
:param src: 源路径
|
||||
:param storage: 源存储类型
|
||||
:param recursive: 是否递归匹配目录子项
|
||||
:return: 命中的成功整理记录
|
||||
"""
|
||||
return TransferHistory.list_success_by_src(
|
||||
self._db,
|
||||
src=src,
|
||||
storage=storage,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
def list_success_move_by_dest(
|
||||
self,
|
||||
dest: str,
|
||||
storage: Optional[str] = None,
|
||||
recursive: bool = False,
|
||||
) -> List[TransferHistory]:
|
||||
"""
|
||||
按目标路径查询成功移动记录。
|
||||
|
||||
:param dest: 目标路径
|
||||
:param storage: 目标存储类型
|
||||
:param recursive: 是否递归匹配目录子项
|
||||
:return: 命中的成功移动记录
|
||||
"""
|
||||
return TransferHistory.list_success_move_by_dest(
|
||||
self._db,
|
||||
dest=dest,
|
||||
storage=storage,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
def list_by_hash(self, download_hash: str) -> List[TransferHistory]:
|
||||
"""
|
||||
按种子hash查询转移记录
|
||||
:param download_hash: 种子hash
|
||||
"""
|
||||
return TransferHistory.list_by_hash(self._db, download_hash)
|
||||
|
||||
def add(self, **kwargs):
|
||||
"""
|
||||
新增转移历史
|
||||
"""
|
||||
kwargs.update({
|
||||
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
})
|
||||
TransferHistory(**kwargs).create(self._db)
|
||||
|
||||
def statistic(self, days: int = 7) -> List[Any]:
|
||||
"""
|
||||
统计最近days天的下载历史数量
|
||||
"""
|
||||
return TransferHistory.statistic(self._db, days)
|
||||
|
||||
def get_by(self, title: Optional[str] = None, year: Optional[str] = None, mtype: Optional[str] = None,
|
||||
season: Optional[str] = None, episode: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None, media_id: Optional[str] = None,
|
||||
dest: Optional[str] = None) -> List[TransferHistory]:
|
||||
"""
|
||||
按类型、标题、年份、季集查询转移记录
|
||||
"""
|
||||
return TransferHistory.list_by(db=self._db,
|
||||
mtype=mtype,
|
||||
title=title,
|
||||
dest=dest,
|
||||
year=year,
|
||||
season=season,
|
||||
episode=episode,
|
||||
media_source=media_source,
|
||||
media_id=media_id)
|
||||
|
||||
def get_by_media_identity(
|
||||
self, media_source: MediaSource, media_id: str,
|
||||
mtype: Optional[str] = None,
|
||||
) -> Optional[TransferHistory]:
|
||||
"""按规范媒体身份和类型查询整理记录。"""
|
||||
return TransferHistory.get_by_media_identity(
|
||||
db=self._db,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
mtype=mtype,
|
||||
)
|
||||
|
||||
def delete(self, historyid):
|
||||
"""
|
||||
删除转移记录
|
||||
"""
|
||||
TransferHistory.delete(self._db, historyid)
|
||||
|
||||
async def async_delete(self, historyid):
|
||||
"""
|
||||
异步删除转移记录。
|
||||
"""
|
||||
await TransferHistory.async_delete(self._db, historyid)
|
||||
|
||||
def truncate(self):
|
||||
"""
|
||||
清空转移记录
|
||||
"""
|
||||
TransferHistory.truncate(self._db)
|
||||
|
||||
def add_force(self, **kwargs) -> Optional[TransferHistory]:
|
||||
"""
|
||||
新增转移历史,并以同源存储的记录为准替换旧记录。
|
||||
"""
|
||||
# 文件项的默认存储是 local;归一化旧调用传入的 None,确保运行时语义与
|
||||
# (src, src_storage) 唯一索引一致。
|
||||
kwargs["src_storage"] = kwargs.get("src_storage") or "local"
|
||||
# 旧记录的清理交给 replace_by_src 按 (src, src_storage) 处理:
|
||||
# 仅按 src 删除会连带删掉其他存储下同路径的记录。
|
||||
kwargs.update({
|
||||
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
})
|
||||
TransferHistory.replace_by_src(self._db, **kwargs)
|
||||
# 保持 add_force 的既有返回契约:返回可被调用方安全读取字段的查询结果,
|
||||
# 而非事务提交后可能已脱离会话的新建实例。
|
||||
return TransferHistory.get_by_src(
|
||||
self._db,
|
||||
kwargs.get("src"),
|
||||
kwargs["src_storage"],
|
||||
)
|
||||
|
||||
def update_download_hash(self, historyid, download_hash):
|
||||
"""
|
||||
补充转移记录download_hash
|
||||
"""
|
||||
TransferHistory.update_download_hash(self._db, historyid, download_hash)
|
||||
|
||||
def list_by_date(self, date: str) -> List[TransferHistory]:
|
||||
"""
|
||||
查询某时间之后的转移历史
|
||||
:param date: 日期
|
||||
"""
|
||||
return TransferHistory.list_by_date(self._db, date)
|
||||
@@ -0,0 +1,59 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.transferpending import TransferPending
|
||||
|
||||
|
||||
class TransferPendingOper(DbOper):
|
||||
"""
|
||||
待整理文件登记管理。
|
||||
|
||||
只保存「存储 + 源文件路径」这一最小事实,用于在进程重启后把没走完整理链的
|
||||
文件重新送回去,避免挂载故障重启后永久漏件。
|
||||
"""
|
||||
|
||||
def register(self, storage: str, src_path: str) -> Optional[TransferPending]:
|
||||
"""
|
||||
登记一个待整理文件。
|
||||
:param storage: 存储
|
||||
:param src_path: 源文件路径
|
||||
:return: 登记记录
|
||||
"""
|
||||
return TransferPending.register(
|
||||
self._db,
|
||||
storage=storage,
|
||||
src_path=src_path,
|
||||
now_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
def discard(self, storage: str, src_path: str) -> int:
|
||||
"""
|
||||
注销一个待整理文件登记。
|
||||
:param storage: 存储
|
||||
:param src_path: 源文件路径
|
||||
:return: 删除的记录数
|
||||
"""
|
||||
return TransferPending.discard(self._db, storage=storage, src_path=src_path)
|
||||
|
||||
def list_all(self, limit: Optional[int] = 5000) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
列出全部待整理登记,供启动回放使用。
|
||||
|
||||
返回纯元组而不是 ORM 实例:回放发生在会话之外,ORM 实例脱离 session
|
||||
后访问属性会触发 DetachedInstanceError。
|
||||
:param limit: 单次回放上限
|
||||
:return: (存储, 源文件路径) 列表
|
||||
"""
|
||||
return [
|
||||
(item.storage, item.src_path)
|
||||
for item in TransferPending.list_all(self._db, limit=limit) or []
|
||||
if item and item.storage and item.src_path
|
||||
]
|
||||
|
||||
def clear(self) -> int:
|
||||
"""
|
||||
清空全部待整理登记。
|
||||
:return: 删除的记录数
|
||||
"""
|
||||
return TransferPending.clear(self._db)
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
用户数据访问。
|
||||
|
||||
认证依赖(get_current_user 等八个)已迁至 app/api/deps.py——那是 HTTP 层的关注点,
|
||||
产出 403/400 而非数据。本模块只保留 UserOper。
|
||||
|
||||
这里不为那八个名字留惰性转发。转发曾是给仓外插件备的软着陆,代价是把
|
||||
app.db.oper.user -> app.api.deps -> app.application.security 这条边永久焊进依赖图:
|
||||
数据访问模块从此在静态分析里牵着整个鉴权栈,而仓内没有任何调用方需要它。插件生态
|
||||
既已确定迭代,就让旧名字直接以 AttributeError 报错——指向明确、当场可改,好过一条
|
||||
悄悄成立的反向依赖。
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.user import User
|
||||
|
||||
|
||||
class UserOper(DbOper):
|
||||
"""
|
||||
用户管理
|
||||
"""
|
||||
|
||||
def list(self) -> List[User]:
|
||||
"""
|
||||
获取用户列表
|
||||
"""
|
||||
return User.list(self._db)
|
||||
|
||||
def add(self, **kwargs):
|
||||
"""
|
||||
新增用户
|
||||
"""
|
||||
user = User(**kwargs)
|
||||
user.create(self._db)
|
||||
|
||||
def get_by_name(self, name: str) -> Optional[User]:
|
||||
"""
|
||||
根据用户名获取用户
|
||||
"""
|
||||
return User.get_by_name(self._db, name)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Optional[User]:
|
||||
"""
|
||||
异步根据用户名获取用户。
|
||||
"""
|
||||
return await User.async_get_by_name(self._db, name)
|
||||
|
||||
async def async_get_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""
|
||||
异步根据用户 ID 获取用户。
|
||||
"""
|
||||
return await User.async_get_by_id(self._db, user_id)
|
||||
|
||||
def get_permissions(self, name: str) -> dict:
|
||||
"""
|
||||
获取用户权限
|
||||
"""
|
||||
user = User.get_by_name(self._db, name)
|
||||
if user:
|
||||
return user.permissions or {}
|
||||
return {}
|
||||
|
||||
def get_settings(self, name: str) -> Optional[dict]:
|
||||
"""
|
||||
获取用户个性化设置,返回None表示用户不存在
|
||||
"""
|
||||
user = User.get_by_name(self._db, name)
|
||||
if user:
|
||||
return user.settings or {}
|
||||
return None
|
||||
|
||||
def get_setting(self, name: str, key: str) -> Optional[str]:
|
||||
"""
|
||||
获取用户个性化设置
|
||||
"""
|
||||
settings = self.get_settings(name)
|
||||
if settings:
|
||||
return settings.get(key)
|
||||
return None
|
||||
|
||||
def get_name(self, **kwargs) -> Optional[str]:
|
||||
"""
|
||||
根据绑定账号获取用户名称
|
||||
"""
|
||||
users = self.list()
|
||||
for user in users:
|
||||
user_setting = user.settings
|
||||
if user_setting:
|
||||
for k, v in kwargs.items():
|
||||
if user_setting.get(k) == str(v):
|
||||
return user.name
|
||||
return None
|
||||
@@ -0,0 +1,86 @@
|
||||
from typing import Any, Union, Dict, Optional
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.userconfig import UserConfig
|
||||
from app.schemas.types import UserConfigKey
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
|
||||
class UserConfigOper(DbOper, metaclass=Singleton):
|
||||
"""
|
||||
用户配置管理
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
加载配置到内存
|
||||
"""
|
||||
super().__init__()
|
||||
self.__USERCONF = {}
|
||||
for item in UserConfig.list(self._db):
|
||||
self.__set_config_cache(username=item.username, key=item.key, value=item.value)
|
||||
|
||||
def set(self, username: str, key: Union[str, UserConfigKey], value: Any):
|
||||
"""
|
||||
设置用户配置
|
||||
"""
|
||||
if isinstance(key, UserConfigKey):
|
||||
key = key.value
|
||||
# 更新内存
|
||||
self.__set_config_cache(username=username, key=key, value=value)
|
||||
# 写入数据库
|
||||
conf = UserConfig.get_by_key(db=self._db, username=username, key=key)
|
||||
if conf:
|
||||
if value:
|
||||
conf.update(self._db, {"value": value})
|
||||
else:
|
||||
conf.delete(self._db, conf.id)
|
||||
else:
|
||||
conf = UserConfig(username=username, key=key, value=value)
|
||||
conf.create(self._db)
|
||||
|
||||
def get(self, username: str, key: Optional[Union[str, UserConfigKey]] = None) -> Any:
|
||||
"""
|
||||
获取用户配置
|
||||
"""
|
||||
if not username:
|
||||
return self.__USERCONF
|
||||
if isinstance(key, UserConfigKey):
|
||||
key = key.value
|
||||
if not key:
|
||||
return self.__get_config_caches(username=username)
|
||||
return self.__get_config_cache(username=username, key=key)
|
||||
|
||||
def __set_config_cache(self, username: str, key: str, value: Any):
|
||||
"""
|
||||
设置配置缓存
|
||||
"""
|
||||
if not username or not key:
|
||||
return
|
||||
cache = self.__USERCONF
|
||||
if not cache:
|
||||
cache = {}
|
||||
user_cache = cache.get(username)
|
||||
if not user_cache:
|
||||
user_cache = {}
|
||||
cache[username] = user_cache
|
||||
user_cache[key] = value
|
||||
self.__USERCONF = cache
|
||||
|
||||
def __get_config_caches(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取配置缓存
|
||||
"""
|
||||
if not username or not self.__USERCONF:
|
||||
return None
|
||||
return self.__USERCONF.get(username)
|
||||
|
||||
def __get_config_cache(self, username: str, key: str) -> Any:
|
||||
"""
|
||||
获取配置缓存
|
||||
"""
|
||||
if not username or not key or not self.__USERCONF:
|
||||
return None
|
||||
user_cache = self.__get_config_caches(username)
|
||||
if not user_cache:
|
||||
return None
|
||||
return user_cache.get(key)
|
||||
@@ -0,0 +1,110 @@
|
||||
from typing import List, Tuple, Optional, Any, Coroutine, Sequence
|
||||
|
||||
from app.db import DbOper
|
||||
from app.db.models.workflow import Workflow
|
||||
|
||||
|
||||
class WorkflowOper(DbOper):
|
||||
"""
|
||||
工作流管理
|
||||
"""
|
||||
|
||||
def add(self, **kwargs) -> Tuple[bool, str]:
|
||||
"""
|
||||
新增工作流
|
||||
"""
|
||||
wf = Workflow(**kwargs)
|
||||
if not wf.get_by_name(self._db, kwargs.get("name")):
|
||||
wf.create(self._db)
|
||||
return True, "新增工作流成功"
|
||||
return False, "工作流已存在"
|
||||
|
||||
def get(self, wid: int) -> Optional[Workflow]:
|
||||
"""
|
||||
查询单个工作流
|
||||
"""
|
||||
return Workflow.get(self._db, wid)
|
||||
|
||||
async def async_get(self, wid: int) -> Optional[Workflow]:
|
||||
"""
|
||||
异步查询单个工作流
|
||||
"""
|
||||
return await Workflow.async_get(self._db, wid)
|
||||
|
||||
def list(self) -> List[Workflow]:
|
||||
"""
|
||||
获取所有工作流列表
|
||||
"""
|
||||
return Workflow.list(self._db)
|
||||
|
||||
async def async_list(self) -> List[Workflow]:
|
||||
"""
|
||||
异步获取所有工作流列表
|
||||
"""
|
||||
return await Workflow.async_list(self._db)
|
||||
|
||||
def list_enabled(self) -> List[Workflow]:
|
||||
"""
|
||||
获取启用的工作流列表
|
||||
"""
|
||||
return Workflow.get_enabled_workflows(self._db)
|
||||
|
||||
def get_timer_triggered_workflows(self) -> List[Workflow]:
|
||||
"""
|
||||
获取定时触发的工作流列表
|
||||
"""
|
||||
return Workflow.get_timer_triggered_workflows(self._db)
|
||||
|
||||
def get_event_triggered_workflows(self) -> List[Workflow]:
|
||||
"""
|
||||
获取事件触发的工作流列表
|
||||
"""
|
||||
return Workflow.get_event_triggered_workflows(self._db)
|
||||
|
||||
def get_by_name(self, name: str) -> Workflow:
|
||||
"""
|
||||
按名称获取工作流
|
||||
"""
|
||||
return Workflow.get_by_name(self._db, name)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Optional[Workflow]:
|
||||
"""
|
||||
异步按名称获取工作流
|
||||
"""
|
||||
return await Workflow.async_get_by_name(self._db, name)
|
||||
|
||||
def start(self, wid: int) -> bool:
|
||||
"""
|
||||
启动
|
||||
"""
|
||||
return Workflow.start(self._db, wid)
|
||||
|
||||
def success(self, wid: int, result: Optional[str] = None) -> bool:
|
||||
"""
|
||||
成功
|
||||
"""
|
||||
return Workflow.success(self._db, wid, result)
|
||||
|
||||
def fail(self, wid: int, result: str) -> bool:
|
||||
"""
|
||||
失败
|
||||
"""
|
||||
return Workflow.fail(self._db, wid, result)
|
||||
|
||||
def step(self, wid: int, action_id: str, context: dict, execution_state: Optional[dict] = None) -> bool:
|
||||
"""
|
||||
步进
|
||||
"""
|
||||
return Workflow.update_current_action(
|
||||
self._db,
|
||||
wid,
|
||||
action_id,
|
||||
context,
|
||||
execution_state
|
||||
)
|
||||
|
||||
def reset(self, wid: int, reset_count: bool = False) -> bool:
|
||||
"""
|
||||
重置
|
||||
"""
|
||||
return Workflow.reset(self._db, wid, reset_count=reset_count)
|
||||
Reference in New Issue
Block a user