refactor(db): 修复异步连接池无界增长,并完成 SQLAlchemy 2.0 迁移与分层归位 (#6320)

This commit is contained in:
Aqr-K
2026-08-15 06:58:38 +08:00
committed by GitHub
parent e28de9cfe1
commit 8a11214a43
252 changed files with 11405 additions and 2889 deletions
+7
View File
@@ -1,3 +1,10 @@
"""
ORM 模型。
_identity 必须在此处导入:它在 import 期把媒体身份归一挂到 mapper 事件上,是六张带
身份列的表的写入不变量。导入任一模型都会先初始化本包,因此这一行让强制点无处可绕。
"""
from . import _identity # noqa: F401 仅为注册 mapper 事件,不导出符号
from .agentchat import AgentChat
from .agenttask import AgentTask
from .agenttaskrun import AgentTaskRun
@@ -1,3 +1,11 @@
"""建表约束的共享片段——本模块不声明任何表,只被同包的模型模块拼进 ``__table_args__``。
以下划线开头且不进 ``models/__init__.py`` 的再导出是为了与同目录一实体一文件
模块区分开 ``media_identity.py`` 时它看着就像一张 MediaIdentity
注意alembic 迁移脚本必须自带 SQL 常量的副本而不是 import 本模块迁移是历史快照
跟着当前代码一起演进会让旧库重放出新约束
"""
from sqlalchemy import CheckConstraint
MEDIA_IDENTITY_CHECK_SQL = (
+71
View File
@@ -0,0 +1,71 @@
"""
媒体身份的持久化不变量。
「media_source 与 media_id 必须成对、非零、去空白」这条规则此前由六张表的各个 Oper
在建模前各调一次 normalize_media_identity_payload 来保证——靠调用点的纪律,新加一条
写入路径忘了调,就会静静写进半对身份,而按身份去重从此对这行失效。
这里把它下沉成 flush 前的 mapper 事件:凡同时具备两列的表,任何 ORM 写入都会经过,
忘不掉也绕不开。app/db 里没有 core insert()/bulk 写法(已核对),因此覆盖是完整的。
与 DTO 侧的失败语义有意不同,见下方 _normalize_identity 的说明。
"""
from typing import Any
from sqlalchemy import event
from sqlalchemy.orm import Mapper
from app.runtime.log import logger
from app.schemas.media import resolve_media_identity
# 构成媒体身份的两列,缺一不可
IDENTITY_COLUMNS = ("media_source", "media_id")
def _normalize_identity(mapper: Mapper, connection: Any, target: Any) -> None:
"""
写库前归一媒体身份;半对、非法或零值身份清空两列并记一条告警。
为什么是「清空 + 告警」而不是像 DTO 侧那样抛错:这六张表都是记账性写入(整理历史、
下载历史、失败冷却、媒体服务器同步、订阅历史)。因身份不成对就让整条记录写不进去,
等于用一个次要字段的问题换掉整条记账——而丢一条整理历史意味着那个文件可能被重复
整理。所以持久化侧选择降级保留,但**不再沉默**:告警让半对身份从「查不出的脏数据」
变成「日志里可检索的事件」。DTO 侧仍然抛错,那里是用户输入的边界,该当场拒绝。
:param mapper: 触发事件的映射器
:param connection: 本次 flush 使用的连接,未用到
:param target: 待写入的模型实例
"""
columns = mapper.columns.keys()
if not all(name in columns for name in IDENTITY_COLUMNS):
return
raw_source = getattr(target, "media_source", None)
raw_id = getattr(target, "media_id", None)
if raw_source is None and raw_id is None:
return
media_source, media_id = resolve_media_identity(
media_source=raw_source, media_id=raw_id
)
if not (media_source and media_id):
# 用映射类名而非 local_table.name:后者的静态类型是 FromClause,没有 name
logger.warn(
f"{mapper.class_.__name__} 的媒体身份不成对,已清空:"
f"media_source={raw_source!r}, media_id={raw_id!r}"
)
target.media_source = media_source.value if media_source else None
target.media_id = media_id
def register_identity_normalizer() -> None:
"""
注册身份归一事件。
监听 Mapper 类本身而非某个基类:Base 自身没有表、不是映射类,挂不上 mapper 事件;
挂在 Mapper 上则覆盖进程内全部映射,包括仓外插件自建的模型。开销由上面那行列名
检查兜住——不具备身份列的表直接返回。
"""
event.listen(Mapper, "before_insert", _normalize_identity)
event.listen(Mapper, "before_update", _normalize_identity)
register_identity_normalizer()
+32 -33
View File
@@ -1,8 +1,8 @@
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Column, Integer, String, JSON, Index, select
from sqlalchemy import Integer, String, JSON, Index, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, async_db_query, db_query, get_id_column
@@ -14,33 +14,33 @@ class AgentChat(Base):
id = get_id_column()
# Agent 内部会话 ID,用于恢复 LangGraph 对话上下文
session_id = Column(String, nullable=False)
session_id: Mapped[str] = mapped_column(String, nullable=False)
# 前端或渠道侧传入的原始会话标识
client_session_id = Column(String)
client_session_id: Mapped[Optional[str]] = mapped_column(String)
# 用户 ID
user_id = Column(String)
user_id: Mapped[Optional[str]] = mapped_column(String)
# 用户名称
username = Column(String)
username: Mapped[Optional[str]] = mapped_column(String)
# 消息渠道
channel = Column(String)
channel: Mapped[Optional[str]] = mapped_column(String)
# 渠道来源配置名
source = Column(String)
source: Mapped[Optional[str]] = mapped_column(String)
# 原聊天 ID,用于区分群聊、频道或私聊
original_chat_id = Column(String)
original_chat_id: Mapped[Optional[str]] = mapped_column(String)
# 会话标题
title = Column(String)
title: Mapped[Optional[str]] = mapped_column(String)
# 会话预览文本
preview = Column(String)
preview: Mapped[Optional[str]] = mapped_column(String)
# 原始 LangChain messages,用于继续会话
agent_messages = Column(JSON)
agent_messages: Mapped[Optional[Any]] = mapped_column(JSON)
# 展示给用户的消息记录,包含文字、工具提示、附件与选择卡片
display_messages = Column(JSON)
display_messages: Mapped[Optional[Any]] = mapped_column(JSON)
# 展示消息数量
message_count = Column(Integer, default=0)
message_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 创建时间
created_at = Column(String)
created_at: Mapped[Optional[str]] = mapped_column(String)
# 更新时间
updated_at = Column(String)
updated_at: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
Index("ix_agentchat_session_user", "session_id", "user_id"),
@@ -56,10 +56,10 @@ class AgentChat(Base):
"""
根据会话 ID 获取 Agent 会话。
"""
query = db.query(cls).filter(cls.session_id == session_id)
statement = select(cls).where(cls.session_id == session_id)
if user_id is not None:
query = query.filter(cls.user_id == user_id)
return query.order_by(cls.id.desc()).first()
statement = statement.where(cls.user_id == user_id)
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
@classmethod
@async_db_query
@@ -80,35 +80,34 @@ class AgentChat(Base):
def list_by_page(
cls,
db: Session,
page: Optional[int] = 1,
count: Optional[int] = 30,
page: int = 1,
count: int = 30,
user_id: Optional[str] = None,
username: Optional[str] = None,
) -> list["AgentChat"]:
"""
分页获取 Agent 会话历史。
"""
query = db.query(cls)
statement = select(cls)
if user_id is not None and username is not None:
query = query.filter((cls.user_id == user_id) | (cls.username == username))
statement = statement.where((cls.user_id == user_id) | (cls.username == username))
elif user_id is not None:
query = query.filter(cls.user_id == user_id)
statement = statement.where(cls.user_id == user_id)
elif username is not None:
query = query.filter(cls.username == username)
return (
query.order_by(cls.updated_at.desc(), cls.id.desc())
statement = statement.where(cls.username == username)
return list(db.execute(
statement.order_by(cls.updated_at.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
.all()
)
).scalars().all())
@classmethod
@async_db_query
async def async_list_by_page(
cls,
db: AsyncSession,
page: Optional[int] = 1,
count: Optional[int] = 30,
page: int = 1,
count: int = 30,
user_id: Optional[str] = None,
username: Optional[str] = None,
) -> list["AgentChat"]:
@@ -127,4 +126,4 @@ class AgentChat(Base):
.offset((page - 1) * count)
.limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
+34 -32
View File
@@ -1,9 +1,9 @@
from typing import Optional
from sqlalchemy import Boolean, Column, Index, Integer, String, Text
from sqlalchemy.orm import Session
from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, db_query, db_update, get_id_column
from app.db import Base, db_query, db_update, execute_dml, get_id_column
class AgentTask(Base):
@@ -13,34 +13,34 @@ class AgentTask(Base):
id = get_id_column()
# 任务名称
name = Column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
# 交给 Agent 执行的完整任务内容
content = Column(Text, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
# 触发类型:date-单次触发,cron-周期触发
trigger_type = Column(String, nullable=False)
trigger_type: Mapped[str] = mapped_column(String, nullable=False)
# 标准五段 cron 表达式
cron_expression = Column(String)
cron_expression: Mapped[Optional[str]] = mapped_column(String)
# 单次触发时间,使用带时区的 ISO 8601 格式
run_at = Column(String)
run_at: Mapped[Optional[str]] = mapped_column(String)
# 是否继续接受调度
enabled = Column(Boolean, nullable=False, default=True)
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# 创建任务的用户与会话上下文
user_id = Column(String, nullable=False)
username = Column(String)
session_id = Column(String, nullable=False)
channel = Column(String)
source = Column(String)
original_chat_id = Column(String)
user_id: Mapped[str] = mapped_column(String, nullable=False)
username: Mapped[Optional[str]] = mapped_column(String)
session_id: Mapped[str] = mapped_column(String, nullable=False)
channel: Mapped[Optional[str]] = mapped_column(String)
source: Mapped[Optional[str]] = mapped_column(String)
original_chat_id: Mapped[Optional[str]] = mapped_column(String)
# 最近一次执行状态与结果
last_status = Column(String, nullable=False, default="waiting")
last_run_at = Column(String)
last_result = Column(Text)
last_status: Mapped[str] = mapped_column(String, nullable=False, default="waiting")
last_run_at: Mapped[Optional[str]] = mapped_column(String)
last_result: Mapped[Optional[str]] = mapped_column(Text)
# 最新一次真实执行的公开 ID,用于保护 last_* 投影不被旧运行覆盖
last_run_id = Column(String)
last_run_id: Mapped[Optional[str]] = mapped_column(String)
# 已收口执行次数;进程中断的未完成尝试不计入
run_count = Column(Integer, nullable=False, default=0)
created_at = Column(String, nullable=False)
updated_at = Column(String, nullable=False)
run_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
created_at: Mapped[str] = mapped_column(String, nullable=False)
updated_at: Mapped[str] = mapped_column(String, nullable=False)
__table_args__ = (
Index("ix_agenttask_enabled", "enabled"),
@@ -69,10 +69,10 @@ class AgentTask(Base):
"""
按任务 ID 和可选用户 ID 查询 Agent 定时任务。
"""
query = db.query(cls).filter(cls.id == task_id)
statement = select(cls).where(cls.id == task_id)
if user_id is not None:
query = query.filter(cls.user_id == user_id)
return query.first()
statement = statement.where(cls.user_id == user_id)
return db.execute(statement).scalars().first()
@classmethod
@db_query
@@ -85,12 +85,14 @@ class AgentTask(Base):
"""
按用户和启用状态查询 Agent 定时任务。
"""
query = db.query(cls)
statement = select(cls)
if user_id is not None:
query = query.filter(cls.user_id == user_id)
statement = statement.where(cls.user_id == user_id)
if enabled is not None:
query = query.filter(cls.enabled.is_(enabled))
return query.order_by(cls.created_at.desc(), cls.id.desc()).all()
statement = statement.where(cls.enabled.is_(enabled))
return list(db.execute(
statement.order_by(cls.created_at.desc(), cls.id.desc())
).scalars().all())
@classmethod
@db_update
@@ -107,10 +109,10 @@ class AgentTask(Base):
运行状态与配置必须在同一条条件更新中判定,避免执行认领后被并发配置写入
覆盖回可再次执行的状态。
"""
query = db.query(cls).filter(
statement = update(cls).where(
cls.id == task_id,
cls.last_status != "running",
)
if user_id is not None:
query = query.filter(cls.user_id == user_id)
return bool(query.update(payload))
statement = statement.where(cls.user_id == user_id)
return bool(execute_dml(db, statement.values(payload)))
+108 -79
View File
@@ -1,9 +1,9 @@
from typing import Optional
from typing import Any, Dict, List, Optional
from sqlalchemy import Column, Index, Integer, String, Text, update
from sqlalchemy.orm import Session
from sqlalchemy import Index, Integer, String, Text, delete, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, db_query, db_update, get_id_column
from app.db import Base, db_query, db_update, execute_dml, get_id_column
from app.db.models.agenttask import AgentTask
@@ -12,27 +12,27 @@ class AgentTaskRun(Base):
id = get_id_column()
# 对外稳定的运行身份;内部自增主键不进入 Agent 合同
run_id = Column(String, nullable=False)
run_id: Mapped[str] = mapped_column(String, nullable=False)
# 所属计划及触发入口
task_id = Column(Integer, nullable=False)
trigger_source = Column(String, nullable=False)
task_id: Mapped[int] = mapped_column(Integer, nullable=False)
trigger_source: Mapped[str] = mapped_column(String, nullable=False)
# 执行开始时的任务与用户上下文快照
name = Column(String, nullable=False)
content = Column(Text, nullable=False)
trigger_type = Column(String, nullable=False)
cron_expression = Column(String)
run_at = Column(String)
user_id = Column(String, nullable=False)
username = Column(String)
session_id = Column(String, nullable=False)
channel = Column(String)
message_source = Column(String)
original_chat_id = Column(String)
name: Mapped[str] = mapped_column(String, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
trigger_type: Mapped[str] = mapped_column(String, nullable=False)
cron_expression: Mapped[Optional[str]] = mapped_column(String)
run_at: Mapped[Optional[str]] = mapped_column(String)
user_id: Mapped[str] = mapped_column(String, nullable=False)
username: Mapped[Optional[str]] = mapped_column(String)
session_id: Mapped[str] = mapped_column(String, nullable=False)
channel: Mapped[Optional[str]] = mapped_column(String)
message_source: Mapped[Optional[str]] = mapped_column(String)
original_chat_id: Mapped[Optional[str]] = mapped_column(String)
# running-success/failed/interrupted;取消沿用 failed 和明确结果文本
status = Column(String, nullable=False)
started_at = Column(String, nullable=False)
finished_at = Column(String)
result = Column(Text)
status: Mapped[str] = mapped_column(String, nullable=False)
started_at: Mapped[str] = mapped_column(String, nullable=False)
finished_at: Mapped[Optional[str]] = mapped_column(String)
result: Mapped[Optional[str]] = mapped_column(Text)
__table_args__ = (
Index("ix_agenttaskrun_run_id", "run_id", unique=True),
@@ -117,32 +117,37 @@ class AgentTaskRun(Base):
disable_date_task: bool = False,
) -> bool:
"""原子收口精确运行,并仅在仍为最新运行时更新任务投影。"""
run = db.query(cls).filter(
cls.run_id == run_id,
).first()
run = db.execute(
select(cls).where(cls.run_id == run_id)
).scalars().first()
if not run:
return False
status = "success" if success else "failed"
finalized = db.query(cls).filter(
cls.run_id == run_id,
cls.status == "running",
).update(
{
"status": status,
"result": result,
"finished_at": finished_at,
},
synchronize_session=False,
finalized = execute_dml(
db,
update(cls)
.where(
cls.run_id == run_id,
cls.status == "running",
)
.values(
status=status,
result=result,
finished_at=finished_at,
),
execution_options={"synchronize_session": False},
)
if not finalized:
return False
task = db.query(AgentTask).filter(
AgentTask.id == run.task_id,
AgentTask.last_run_id == run_id,
).first()
task = db.execute(
select(AgentTask).where(
AgentTask.id == run.task_id,
AgentTask.last_run_id == run_id,
)
).scalars().first()
if task:
payload = {
payload: Dict[str, Any] = {
"last_status": status,
"last_result": result,
"run_count": AgentTask.run_count + 1,
@@ -155,10 +160,16 @@ class AgentTaskRun(Base):
and task.run_at == run.run_at
):
payload["enabled"] = False
db.query(AgentTask).filter(
AgentTask.id == run.task_id,
AgentTask.last_run_id == run_id,
).update(payload, synchronize_session=False)
execute_dml(
db,
update(AgentTask)
.where(
AgentTask.id == run.task_id,
AgentTask.last_run_id == run_id,
)
.values(**payload),
execution_options={"synchronize_session": False},
)
return True
@classmethod
@@ -171,38 +182,46 @@ class AgentTaskRun(Base):
finished_at: str,
) -> bool:
"""原子标记冷启动时遗留的最新运行及任务投影为结果未知。"""
task = db.query(AgentTask).filter(
AgentTask.id == task_id,
AgentTask.last_status == "running",
).first()
task = db.execute(
select(AgentTask).where(
AgentTask.id == task_id,
AgentTask.last_status == "running",
)
).scalars().first()
if not task:
return False
if task.last_run_id:
interrupted = db.query(cls).filter(
cls.run_id == task.last_run_id,
cls.task_id == task.id,
cls.status == "running",
).update(
{
"status": "interrupted",
"result": result,
"finished_at": finished_at,
},
synchronize_session=False,
interrupted = execute_dml(
db,
update(cls)
.where(
cls.run_id == task.last_run_id,
cls.task_id == task.id,
cls.status == "running",
)
.values(
status="interrupted",
result=result,
finished_at=finished_at,
),
execution_options={"synchronize_session": False},
)
if not interrupted:
return False
return bool(db.query(AgentTask).filter(
AgentTask.id == task.id,
AgentTask.last_status == "running",
AgentTask.last_run_id == task.last_run_id,
).update(
{
"last_status": "interrupted",
"last_result": result,
"updated_at": finished_at,
},
synchronize_session=False,
return bool(execute_dml(
db,
update(AgentTask)
.where(
AgentTask.id == task.id,
AgentTask.last_status == "running",
AgentTask.last_run_id == task.last_run_id,
)
.values(
last_status="interrupted",
last_result=result,
updated_at=finished_at,
),
execution_options={"synchronize_session": False},
))
@classmethod
@@ -214,16 +233,22 @@ class AgentTaskRun(Base):
user_id: Optional[str] = None,
) -> bool:
"""原子删除非运行中任务及其执行历史。"""
query = db.query(AgentTask).filter(
statement = delete(AgentTask).where(
AgentTask.id == task_id,
AgentTask.last_status != "running",
)
if user_id is not None:
query = query.filter(AgentTask.user_id == user_id)
deleted = query.delete(synchronize_session=False)
statement = statement.where(AgentTask.user_id == user_id)
deleted = execute_dml(
db, statement, execution_options={"synchronize_session": False}
)
if not deleted:
return False
db.query(cls).filter(cls.task_id == task_id).delete(synchronize_session=False)
execute_dml(
db,
delete(cls).where(cls.task_id == task_id),
execution_options={"synchronize_session": False},
)
return True
@classmethod
@@ -234,7 +259,9 @@ class AgentTaskRun(Base):
run_id: str,
) -> Optional["AgentTaskRun"]:
"""按公开运行 ID 查询一次执行。"""
return db.query(cls).filter(cls.run_id == run_id).first()
return db.execute(
select(cls).where(cls.run_id == run_id)
).scalars().first()
@classmethod
@db_query
@@ -244,11 +271,13 @@ class AgentTaskRun(Base):
task_id: int,
user_id: Optional[str] = None,
limit: int = 10,
) -> list["AgentTaskRun"]:
) -> List["AgentTaskRun"]:
"""按父任务 owner 校验后返回最近的有界运行历史。"""
query = db.query(cls).join(AgentTask, AgentTask.id == cls.task_id).filter(
statement = select(cls).join(AgentTask, AgentTask.id == cls.task_id).where(
cls.task_id == task_id,
)
if user_id is not None:
query = query.filter(AgentTask.user_id == user_id)
return query.order_by(cls.started_at.desc(), cls.id.desc()).limit(limit).all()
statement = statement.where(AgentTask.user_id == user_id)
return list(db.execute(
statement.order_by(cls.started_at.desc(), cls.id.desc()).limit(limit)
).scalars().all())
+39 -37
View File
@@ -1,10 +1,10 @@
from typing import List, Optional
from sqlalchemy import Column, Float, Index, Integer, String
from sqlalchemy.orm import Session
from sqlalchemy import Float, Index, Integer, String, delete, select
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, db_query, db_update, get_id_column
from app.db.models.media_identity import media_identity_constraint
from app.db import Base, db_query, db_update, execute_dml, get_id_column
from app.db.models._constraints import media_identity_constraint
class DownloadFailure(Base):
@@ -14,44 +14,44 @@ class DownloadFailure(Base):
id = get_id_column()
# 资源失败指纹
fingerprint = Column(String, nullable=False)
fingerprint: Mapped[str] = mapped_column(String, nullable=False)
# 类型 电影/电视剧
type = Column(String)
type: Mapped[Optional[str]] = mapped_column(String)
# 标题
title = Column(String)
title: Mapped[Optional[str]] = mapped_column(String)
# 年份
year = Column(String)
year: Mapped[Optional[str]] = mapped_column(String)
# 媒体数据源与原生ID
media_source = Column(String)
media_id = Column(String)
media_source: Mapped[Optional[str]] = mapped_column(String)
media_id: Mapped[Optional[str]] = mapped_column(String)
# Sxx
seasons = Column(String)
seasons: Mapped[Optional[str]] = mapped_column(String)
# Exx
episodes = Column(String)
episodes: Mapped[Optional[str]] = mapped_column(String)
# 站点ID
site = Column(Integer)
site: Mapped[Optional[int]] = mapped_column(Integer)
# 站点名称
site_name = Column(String)
site_name: Mapped[Optional[str]] = mapped_column(String)
# 种子资源键
torrent_id = Column(String)
torrent_id: Mapped[Optional[str]] = mapped_column(String)
# 种子名称
torrent_name = Column(String)
torrent_name: Mapped[Optional[str]] = mapped_column(String)
# 种子大小
torrent_size = Column(Float)
torrent_size: Mapped[Optional[float]] = mapped_column(Float)
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
# 下载来源
source = Column(String)
source: Mapped[Optional[str]] = mapped_column(String)
# 失败原因
error_message = Column(String)
error_message: Mapped[Optional[str]] = mapped_column(String)
# 重试次数
retry_count = Column(Integer, default=0)
retry_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 首次失败时间
first_failed_at = Column(String)
first_failed_at: Mapped[Optional[str]] = mapped_column(String)
# 最近失败时间
last_failed_at = Column(String)
last_failed_at: Mapped[Optional[str]] = mapped_column(String)
# 下次允许重试时间
next_retry_at = Column(String)
next_retry_at: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
media_identity_constraint("downloadfailure"),
@@ -74,11 +74,10 @@ class DownloadFailure(Base):
normalized = list(dict.fromkeys([fingerprint for fingerprint in fingerprints if fingerprint]))
if not normalized:
return []
return (
db.query(cls)
.filter(cls.fingerprint.in_(normalized), cls.next_retry_at > now_time)
.all()
)
return list(db.execute(
select(cls)
.where(cls.fingerprint.in_(normalized), cls.next_retry_at > now_time)
).scalars().all())
@classmethod
@db_update
@@ -93,7 +92,9 @@ class DownloadFailure(Base):
"""
新增或更新资源失败记录。
"""
failure = db.query(cls).filter(cls.fingerprint == fingerprint).first()
failure = db.execute(
select(cls).where(cls.fingerprint == fingerprint)
).scalars().first()
payload = {
**kwargs,
"fingerprint": fingerprint,
@@ -125,14 +126,15 @@ class DownloadFailure(Base):
"""
分批清理已过期较久的失败冷却记录。
"""
ids = [
row[0]
for row in db.query(cls.id)
.filter(cls.next_retry_at < before_time)
ids = db.execute(
select(cls.id)
.where(cls.next_retry_at < before_time)
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
return db.query(cls).filter(cls.id.in_(ids)).delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
+127 -226
View File
@@ -1,12 +1,12 @@
import time
from typing import List, Optional
from typing import Any, List, Optional
from sqlalchemy import Column, Integer, String, JSON, Index, select, func
from sqlalchemy import Integer, String, JSON, Index, delete, select, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, Base, async_db_query
from app.db.models.media_identity import media_identity_constraint
from app.db import Base, async_db_query, db_query, db_update, execute_dml, get_id_column
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MediaSource
@@ -22,51 +22,51 @@ class DownloadHistory(Base):
id = get_id_column()
# 保存路径
path = Column(String, nullable=False, index=True)
path: Mapped[str] = mapped_column(String, nullable=False, index=True)
# 类型 电影/电视剧/音乐
type = Column(String, nullable=False)
type: Mapped[str] = mapped_column(String, nullable=False)
# 标题
title = Column(String, nullable=False)
title: Mapped[str] = mapped_column(String, nullable=False)
# 年份
year = Column(String)
media_source = Column(String, index=True)
media_id = Column(String, index=True)
year: Mapped[Optional[str]] = mapped_column(String)
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 音乐实体类型:recording 单曲、album 专辑
music_type = Column(String)
music_type: Mapped[Optional[str]] = mapped_column(String)
# Sxx
seasons = Column(String)
seasons: Mapped[Optional[str]] = mapped_column(String)
# Exx
episodes = Column(String)
episodes: Mapped[Optional[str]] = mapped_column(String)
# 背景图
image = Column(String)
image: Mapped[Optional[str]] = mapped_column(String)
# 海报
poster = Column(String)
poster: Mapped[Optional[str]] = mapped_column(String)
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
# 下载任务Hash
download_hash = Column(String)
download_hash: Mapped[Optional[str]] = mapped_column(String)
# 种子名称
torrent_name = Column(String)
torrent_name: Mapped[Optional[str]] = mapped_column(String)
# 种子描述
torrent_description = Column(String)
torrent_description: Mapped[Optional[str]] = mapped_column(String)
# 种子站点
torrent_site = Column(String)
torrent_site: Mapped[Optional[str]] = mapped_column(String)
# 下载用户
userid = Column(String)
userid: Mapped[Optional[str]] = mapped_column(String)
# 下载用户名/插件名
username = Column(String)
username: Mapped[Optional[str]] = mapped_column(String)
# 下载渠道
channel = Column(String)
channel: Mapped[Optional[str]] = mapped_column(String)
# 创建时间
date = Column(String)
date: Mapped[Optional[str]] = mapped_column(String)
# 附加信息
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
# 自定义媒体类别
media_category = Column(String)
media_category: Mapped[Optional[str]] = mapped_column(String)
# 剧集组
episode_group = Column(String)
episode_group: Mapped[Optional[str]] = mapped_column(String)
# 自定义识别词(用于整理时应用)
custom_words = Column(String)
custom_words: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
media_identity_constraint("downloadhistory"),
@@ -78,12 +78,11 @@ class DownloadHistory(Base):
@classmethod
@db_query
def get_by_hash(cls, db: Session, download_hash: str):
return (
db.query(DownloadHistory)
.filter(DownloadHistory.download_hash == download_hash)
return db.execute(
select(DownloadHistory)
.where(DownloadHistory.download_hash == download_hash)
.order_by(DownloadHistory.date.desc())
.first()
)
).scalars().first()
@classmethod
@db_query
@@ -102,12 +101,11 @@ class DownloadHistory(Base):
if not normalized_hashes:
return []
histories = (
db.query(DownloadHistory)
.filter(DownloadHistory.download_hash.in_(normalized_hashes))
histories = db.execute(
select(DownloadHistory)
.where(DownloadHistory.download_hash.in_(normalized_hashes))
.order_by(DownloadHistory.download_hash, DownloadHistory.date.desc())
.all()
)
).scalars().all()
latest_histories = {}
for history in histories:
if history.download_hash and history.download_hash not in latest_histories:
@@ -128,32 +126,30 @@ class DownloadHistory(Base):
"""按规范媒体身份查询下载历史。"""
if not media_source or media_id is None or not str(media_id).strip():
return []
query = db.query(DownloadHistory)
query = query.filter(
statement = select(DownloadHistory).where(
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id).strip(),
)
if music_type:
query = query.filter(DownloadHistory.music_type == music_type)
return query.all()
statement = statement.where(DownloadHistory.music_type == music_type)
return list(db.execute(statement).scalars().all())
@classmethod
@db_query
def list_by_page(
cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30
cls, db: Session, page: int = 1, count: int = 30
):
return (
db.query(DownloadHistory)
return list(db.execute(
select(DownloadHistory)
.order_by(DownloadHistory.date.desc(), DownloadHistory.id.desc())
.offset((page - 1) * count)
.limit(count)
.all()
)
).scalars().all())
@classmethod
@async_db_query
async def async_list_by_page(
cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30
cls, db: AsyncSession, page: int = 1, count: int = 30
):
result = await db.execute(
select(cls)
@@ -161,7 +157,7 @@ class DownloadHistory(Base):
.offset((page - 1) * count)
.limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@async_db_query
@@ -169,15 +165,15 @@ class DownloadHistory(Base):
cls,
db: AsyncSession,
title: str,
page: Optional[int] = 1,
count: Optional[int] = 30,
page: int = 1,
count: int = 30,
):
query = (
select(cls).filter(_title_like(cls.title, title)).order_by(cls.date.desc())
)
query = query.offset((page - 1) * count).limit(count)
result = await db.execute(query)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@async_db_query
@@ -196,7 +192,9 @@ class DownloadHistory(Base):
@classmethod
@db_query
def get_by_path(cls, db: Session, path: str):
return db.query(DownloadHistory).filter(DownloadHistory.path == path).first()
return db.execute(
select(DownloadHistory).where(DownloadHistory.path == path)
).scalars().first()
@classmethod
@db_query
@@ -215,107 +213,46 @@ class DownloadHistory(Base):
按媒体身份、季集或标题年份查询下载记录。
"""
if media_source and media_id and mtype:
# 电视剧某季某集
if season is not None and episode:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
DownloadHistory.type == mtype,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode,
)
.order_by(DownloadHistory.id.desc())
.all()
)
# 电视剧某季
elif season is not None:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
DownloadHistory.type == mtype,
DownloadHistory.seasons == season,
)
.order_by(DownloadHistory.id.desc())
.all()
)
else:
# 电视剧所有季集/电影
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
DownloadHistory.type == mtype,
)
.order_by(DownloadHistory.id.desc())
.all()
)
# 标题 + 年份
statement = select(DownloadHistory).where(
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
DownloadHistory.type == mtype,
)
elif title and year:
# 电视剧某季某集
if season is not None and episode:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode,
)
.order_by(DownloadHistory.id.desc())
.all()
)
# 电视剧某季
elif season is not None:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
)
.order_by(DownloadHistory.id.desc())
.all()
)
else:
# 电视剧所有季集/电影
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.title == title, DownloadHistory.year == year
)
.order_by(DownloadHistory.id.desc())
.all()
)
statement = select(DownloadHistory).where(
DownloadHistory.title == title,
DownloadHistory.year == year,
)
else:
return []
# 季、集逐级收窄:给出季才可能给集,与原六条分支等价
if season is not None:
statement = statement.where(DownloadHistory.seasons == season)
if episode:
statement = statement.where(DownloadHistory.episodes == episode)
return list(db.execute(
statement.order_by(DownloadHistory.id.desc())
).scalars().all())
return []
@classmethod
@db_query
def list_by_user_date(cls, db: Session, date: str, username: Optional[str] = None):
"""
查询某用户某时间之的下载历史
查询某用户某时间之的下载历史
条件是 date < 传入时刻,等于该时刻的那条不计入;oper 层的同名方法描述一致。
:param db: 数据库会话
:param date: 时间水位,取该时刻之前的记录
:param username: 下载用户,不传则跨用户返回
:return: 下载历史列表,按主键倒序
"""
statement = select(DownloadHistory).where(DownloadHistory.date < date)
if username:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.date < date, DownloadHistory.username == username
)
.order_by(DownloadHistory.id.desc())
.all()
)
else:
return (
db.query(DownloadHistory)
.filter(DownloadHistory.date < date)
.order_by(DownloadHistory.id.desc())
.all()
)
statement = statement.where(DownloadHistory.username == username)
return list(db.execute(
statement.order_by(DownloadHistory.id.desc())
).scalars().all())
@classmethod
@db_query
@@ -331,46 +268,30 @@ class DownloadHistory(Base):
"""
查询某时间之后的下载历史
"""
statement = select(DownloadHistory).where(
DownloadHistory.date > date,
DownloadHistory.type == type,
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
)
if seasons:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.date > date,
DownloadHistory.type == type,
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
DownloadHistory.seasons == seasons,
)
.order_by(DownloadHistory.id.desc())
.all()
)
else:
return (
db.query(DownloadHistory)
.filter(
DownloadHistory.date > date,
DownloadHistory.type == type,
DownloadHistory.media_source == str(media_source),
DownloadHistory.media_id == str(media_id),
)
.order_by(DownloadHistory.id.desc())
.all()
)
statement = statement.where(DownloadHistory.seasons == seasons)
return list(db.execute(
statement.order_by(DownloadHistory.id.desc())
).scalars().all())
@classmethod
@db_query
def list_by_type(cls, db: Session, mtype: str, days: int):
return (
db.query(DownloadHistory)
.filter(
return list(db.execute(
select(DownloadHistory).where(
DownloadHistory.type == mtype,
DownloadHistory.date
>= time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 86400 * int(days))
),
)
.all()
)
).scalars().all())
@classmethod
@db_update
@@ -383,20 +304,17 @@ class DownloadHistory(Base):
"""
分批删除指定时间之前的下载历史。
"""
ids = [
row[0]
for row in db.query(cls.id)
.filter(cls.date < before_time)
ids = db.execute(
select(cls.id)
.where(cls.date < before_time)
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
return (
db.query(cls)
.filter(cls.id.in_(ids))
.delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
@@ -407,19 +325,19 @@ class DownloadFiles(Base):
id = get_id_column()
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
# 下载任务Hash
download_hash = Column(String)
download_hash: Mapped[Optional[str]] = mapped_column(String)
# 完整路径
fullpath = Column(String)
fullpath: Mapped[Optional[str]] = mapped_column(String)
# 保存路径
savepath = Column(String, index=True)
savepath: Mapped[Optional[str]] = mapped_column(String, index=True)
# 文件相对路径/名称
filepath = Column(String)
filepath: Mapped[Optional[str]] = mapped_column(String)
# 种子名称
torrentname = Column(String)
torrentname: Mapped[Optional[str]] = mapped_column(String)
# 状态 0-已删除 1-正常
state = Column(Integer, nullable=False, default=1)
state: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
__table_args__ = (
Index('ix_downloadfiles_download_hash_state', 'download_hash', 'state'),
@@ -429,43 +347,29 @@ class DownloadFiles(Base):
@classmethod
@db_query
def get_by_hash(cls, db: Session, download_hash: str, state: Optional[int] = None):
statement = select(cls).where(cls.download_hash == download_hash)
if state is not None:
return (
db.query(cls)
.filter(cls.download_hash == download_hash, cls.state == state)
.all()
)
else:
return db.query(cls).filter(cls.download_hash == download_hash).all()
statement = statement.where(cls.state == state)
return list(db.execute(statement).scalars().all())
@classmethod
@db_query
def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False):
if not all_files:
return (
db.query(cls)
.filter(cls.fullpath == fullpath)
.order_by(cls.id.desc())
.first()
)
else:
return (
db.query(cls)
.filter(cls.fullpath == fullpath)
.order_by(cls.id.desc())
.all()
)
result = db.execute(
select(cls).where(cls.fullpath == fullpath).order_by(cls.id.desc())
).scalars()
return list(result.all()) if all_files else result.first()
@classmethod
@db_query
def get_by_savepath(cls, db: Session, savepath: str):
return db.query(cls).filter(cls.savepath == savepath).all()
return list(db.execute(select(cls).where(cls.savepath == savepath)).scalars().all())
@classmethod
@db_update
def delete_by_fullpath(cls, db: Session, fullpath: str):
db.query(cls).filter(cls.fullpath == fullpath, cls.state == 1).update(
{"state": 0}
db.execute(
update(cls).where(cls.fullpath == fullpath, cls.state == 1).values(state=0)
)
@classmethod
@@ -481,22 +385,19 @@ class DownloadFiles(Base):
downloadfiles 没有时间字段,无法安全地按时间直接裁剪,
因此只清理明确失去父记录的孤儿数据。
"""
ids = [
row[0]
for row in db.query(cls.id)
ids = db.execute(
select(cls.id)
.outerjoin(
DownloadHistory,
DownloadHistory.download_hash == cls.download_hash,
)
.filter(DownloadHistory.id.is_(None))
.where(DownloadHistory.id.is_(None))
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
return (
db.query(cls)
.filter(cls.id.in_(ids))
.delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
+50 -45
View File
@@ -1,13 +1,13 @@
from datetime import datetime
from typing import Optional, List
from typing import Any, List, Optional
from sqlalchemy import Column, Integer, String, JSON, Index, or_
from sqlalchemy import Integer, String, JSON, Index, delete, or_
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, async_db_query, Base
from app.db.models.media_identity import media_identity_constraint
from app.db import Base, async_db_query, db_query, db_update, execute_dml, get_id_column
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MediaSource
@@ -17,30 +17,30 @@ class MediaServerItem(Base):
"""
id = get_id_column()
# 服务器类型
server = Column(String)
server: Mapped[Optional[str]] = mapped_column(String)
# 媒体库ID
library = Column(String)
library: Mapped[Optional[str]] = mapped_column(String)
# ID
item_id = Column(String, index=True)
item_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 类型
item_type = Column(String)
item_type: Mapped[Optional[str]] = mapped_column(String)
# 标题
title = Column(String, index=True)
title: Mapped[Optional[str]] = mapped_column(String, index=True)
# 原标题
original_title = Column(String)
original_title: Mapped[Optional[str]] = mapped_column(String)
# 年份
year = Column(String)
year: Mapped[Optional[str]] = mapped_column(String)
# 媒体数据源与原生ID
media_source = Column(String, index=True)
media_id = Column(String, index=True)
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 路径
path = Column(String)
path: Mapped[Optional[str]] = mapped_column(String)
# 季集
seasoninfo = Column(JSON, default=dict)
seasoninfo: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 备注
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
# 同步时间
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
lst_mod_date: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
__table_args__ = (
media_identity_constraint("mediaserveritem"),
@@ -54,36 +54,46 @@ class MediaServerItem(Base):
@classmethod
@db_query
def get_by_itemid(cls, db: Session, item_id: str):
return db.query(cls).filter(cls.item_id == item_id).first()
return db.execute(select(cls).where(cls.item_id == item_id)).scalars().first()
@classmethod
@db_query
def get_by_server_itemid(cls, db: Session, server: str, item_id: str):
return db.query(cls).filter(cls.server == server,
cls.item_id == item_id).first()
return db.execute(
select(cls).where(cls.server == server, cls.item_id == item_id)
).scalars().first()
@classmethod
@db_update
def empty(cls, db: Session, server: Optional[str] = None):
if server is None:
db.query(cls).delete(synchronize_session=False)
else:
db.query(cls).filter(cls.server == server).delete(synchronize_session=False)
statement = delete(cls)
if server is not None:
statement = statement.where(cls.server == server)
db.execute(statement, execution_options={"synchronize_session": False})
@classmethod
@db_update
def delete_stale(cls, db: Session, server: str, sync_time: str):
return db.query(cls).filter(cls.server == server,
or_(cls.lst_mod_date.is_(None),
cls.lst_mod_date != sync_time)).delete(synchronize_session=False)
return execute_dml(
db,
delete(cls).where(
cls.server == server,
or_(cls.lst_mod_date.is_(None), cls.lst_mod_date != sync_time),
),
execution_options={"synchronize_session": False},
)
@classmethod
@db_update
def delete_excluded_servers(cls, db: Session, servers: List[str]):
if not servers:
return db.query(cls).delete(synchronize_session=False)
return db.query(cls).filter(or_(cls.server.is_(None),
~cls.server.in_(servers))).delete(synchronize_session=False)
statement = delete(cls)
if servers:
statement = statement.where(
or_(cls.server.is_(None), ~cls.server.in_(servers))
)
return execute_dml(
db, statement, execution_options={"synchronize_session": False}
)
@classmethod
@db_query
@@ -91,26 +101,21 @@ class MediaServerItem(Base):
cls, db: Session, media_source: MediaSource, media_id: str, mtype: str,
):
"""按规范媒体身份和类型查询媒体服务器条目。"""
return db.query(cls).filter(
return db.execute(select(cls).where(
cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.item_type == mtype,
).first()
)).scalars().first()
@classmethod
@db_query
def exists_by_title(cls, db: Session, title: str, mtype: str, year: str):
if not mtype and not year:
return db.query(cls).filter(cls.title == title).first()
elif not year:
return db.query(cls).filter(cls.title == title,
cls.item_type == mtype).first()
elif not mtype:
return db.query(cls).filter(cls.title == title,
cls.year == str(year)).first()
return db.query(cls).filter(cls.title == title,
cls.item_type == mtype,
cls.year == str(year)).first()
statement = select(cls).where(cls.title == title)
if mtype:
statement = statement.where(cls.item_type == mtype)
if year:
statement = statement.where(cls.year == str(year))
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
+34 -36
View File
@@ -1,10 +1,10 @@
from typing import List, Optional
from typing import Any, List, Optional
from sqlalchemy import Column, Integer, String, JSON, Index, and_, or_, select
from sqlalchemy import Integer, String, JSON, Index, and_, delete, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, Base, get_id_column, async_db_query
from app.db import Base, async_db_query, db_query, db_update, execute_dml, get_id_column
class Message(Base):
@@ -13,27 +13,27 @@ class Message(Base):
"""
id = get_id_column()
# 消息渠道
channel = Column(String)
channel: Mapped[Optional[str]] = mapped_column(String)
# 消息来源
source = Column(String)
source: Mapped[Optional[str]] = mapped_column(String)
# 消息类型
mtype = Column(String)
mtype: Mapped[Optional[str]] = mapped_column(String)
# 标题
title = Column(String)
title: Mapped[Optional[str]] = mapped_column(String)
# 文本内容
text = Column(String)
text: Mapped[Optional[str]] = mapped_column(String)
# 图片
image = Column(String)
image: Mapped[Optional[str]] = mapped_column(String)
# 链接
link = Column(String)
link: Mapped[Optional[str]] = mapped_column(String)
# 用户ID
userid = Column(String)
userid: Mapped[Optional[str]] = mapped_column(String)
# 登记时间
reg_time = Column(String)
reg_time: Mapped[Optional[str]] = mapped_column(String)
# 消息方向:0-接收息,1-发送消息
action = Column(Integer)
action: Mapped[Optional[int]] = mapped_column(Integer)
# 附件json
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
__table_args__ = (
Index('ix_message_reg_time_id', 'reg_time', 'id'),
@@ -50,17 +50,16 @@ class Message(Base):
@classmethod
@db_query
def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30) -> List["Message"]:
def list_by_page(cls, db: Session, page: int = 1, count: int = 30) -> List["Message"]:
"""
分页获取消息记录。
"""
return (
db.query(cls)
return list(db.execute(
select(cls)
.order_by(cls.reg_time.desc(), cls.id.desc())
.offset((page - 1) * count)
.limit(count)
.all()
)
).scalars().all())
@classmethod
@db_query
@@ -72,12 +71,14 @@ class Message(Base):
:param source: 消息来源唯一标识
:return: 是否存在匹配记录
"""
return db.query(cls.id).filter(cls.source == source).first() is not None
return db.execute(
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
@classmethod
@async_db_query
async def async_list_by_page(
cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30
cls, db: AsyncSession, page: int = 1, count: int = 30
) -> List["Message"]:
"""
异步分页获取消息记录。
@@ -88,15 +89,15 @@ class Message(Base):
.offset((page - 1) * count)
.limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@async_db_query
async def async_list_sent_by_page(
cls,
db: AsyncSession,
page: Optional[int] = 1,
count: Optional[int] = 30,
page: int = 1,
count: int = 30,
all_clear_before: Optional[str] = None,
system_clear_before: Optional[str] = None,
media_clear_before: Optional[str] = None,
@@ -129,7 +130,7 @@ class Message(Base):
.offset((page - 1) * count)
.limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_update
@@ -142,18 +143,15 @@ class Message(Base):
"""
分批删除指定时间之前的消息记录。
"""
ids = [
row[0]
for row in db.query(cls.id)
.filter(cls.reg_time < before_time)
ids = db.execute(
select(cls.id)
.where(cls.reg_time < before_time)
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
return (
db.query(cls)
.filter(cls.id.in_(ids))
.delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
+24 -20
View File
@@ -1,6 +1,7 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, select, ForeignKey
from typing import Optional
from sqlalchemy import Integer, String, Boolean, DateTime, Text, select, ForeignKey
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from datetime import datetime
from app.db import Base, db_query, db_update, async_db_query, async_db_update, get_id_column
@@ -13,31 +14,33 @@ class PassKey(Base):
# ID
id = get_id_column()
# 用户ID
user_id = Column(Integer, ForeignKey('user.id'), nullable=False, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'), nullable=False, index=True)
# 凭证ID (credential_id)
credential_id = Column(String, nullable=False, unique=True, index=True)
credential_id: Mapped[str] = mapped_column(String, nullable=False, unique=True, index=True)
# 凭证公钥
public_key = Column(Text, nullable=False)
public_key: Mapped[str] = mapped_column(Text, nullable=False)
# 签名计数器
sign_count = Column(Integer, default=0)
sign_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 凭证名称(用户自定义)
name = Column(String, default="通行密钥")
name: Mapped[Optional[str]] = mapped_column(String, default="通行密钥")
# AAGUID (Authenticator Attestation GUID)
aaguid = Column(String, nullable=True)
aaguid: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# 创建时间
created_at = Column(DateTime, default=datetime.now)
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime, default=datetime.now)
# 最后使用时间
last_used_at = Column(DateTime, nullable=True)
last_used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
# 是否启用
is_active = Column(Boolean, default=True)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean, default=True)
# 传输方式 (usb, nfc, ble, internal)
transports = Column(String, nullable=True)
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
@classmethod
@db_query
def get_by_user_id(cls, db: Session, user_id: int):
"""获取用户的所有PassKey"""
return db.query(cls).filter(cls.user_id == user_id, cls.is_active.is_(True)).all()
return list(db.execute(
select(cls).where(cls.user_id == user_id, cls.is_active.is_(True))
).scalars().all())
@classmethod
@async_db_query
@@ -46,13 +49,15 @@ class PassKey(Base):
result = await db.execute(
select(cls).filter(cls.user_id == user_id, cls.is_active.is_(True))
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_by_credential_id(cls, db: Session, credential_id: str):
"""根据凭证ID获取PassKey"""
return db.query(cls).filter(cls.credential_id == credential_id, cls.is_active.is_(True)).first()
return db.execute(
select(cls).where(cls.credential_id == credential_id, cls.is_active.is_(True))
).scalars().first()
@classmethod
@async_db_query
@@ -67,7 +72,7 @@ class PassKey(Base):
@db_query
def get_by_id(cls, db: Session, passkey_id: int):
"""根据ID获取PassKey"""
return db.query(cls).filter(cls.id == passkey_id).first()
return db.execute(select(cls).where(cls.id == passkey_id)).scalars().first()
@classmethod
@async_db_query
@@ -82,10 +87,9 @@ class PassKey(Base):
@db_update
def delete_by_id(cls, db: Session, passkey_id: int, user_id: int):
"""删除指定用户的PassKey"""
passkey = db.query(cls).filter(
cls.id == passkey_id,
cls.user_id == user_id
).first()
passkey = db.execute(
select(cls).where(cls.id == passkey_id, cls.user_id == user_id)
).scalars().first()
if passkey:
passkey.delete(db, passkey.id)
return True
+15 -12
View File
@@ -1,6 +1,7 @@
from sqlalchemy import Column, String, JSON, Index, select
from typing import Any, Optional
from sqlalchemy import String, JSON, Index, delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import (
db_query,
@@ -16,9 +17,9 @@ class PluginData(Base):
插件数据表
"""
id = get_id_column()
plugin_id = Column(String, nullable=False)
key = Column(String, nullable=False)
value = Column(JSON)
plugin_id: Mapped[str] = mapped_column(String, nullable=False)
key: Mapped[str] = mapped_column(String, nullable=False)
value: Mapped[Optional[Any]] = mapped_column(JSON)
__table_args__ = (
Index('ix_plugindata_plugin_id_key', 'plugin_id', 'key'),
@@ -27,18 +28,20 @@ class PluginData(Base):
@classmethod
@db_query
def get_plugin_data(cls, db: Session, plugin_id: str):
return db.query(cls).filter(cls.plugin_id == plugin_id).all()
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@async_db_query
async def async_get_plugin_data(cls, db: AsyncSession, plugin_id: str):
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_plugin_data_by_key(cls, db: Session, plugin_id: str, key: str):
return db.query(cls).filter(cls.plugin_id == plugin_id, cls.key == key).first()
return db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
).scalars().first()
@classmethod
@async_db_query
@@ -53,17 +56,17 @@ class PluginData(Base):
@classmethod
@db_update
def del_plugin_data_by_key(cls, db: Session, plugin_id: str, key: str):
db.query(cls).filter(cls.plugin_id == plugin_id, cls.key == key).delete()
db.execute(delete(cls).where(cls.plugin_id == plugin_id, cls.key == key))
@classmethod
@db_update
def del_plugin_data(cls, db: Session, plugin_id: str):
db.query(cls).filter(cls.plugin_id == plugin_id).delete()
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
@classmethod
@db_query
def get_plugin_data_by_plugin_id(cls, db: Session, plugin_id: str):
return db.query(cls).filter(cls.plugin_id == plugin_id).all()
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@async_db_query
@@ -71,4 +74,4 @@ class PluginData(Base):
cls, db: AsyncSession, plugin_id: str
):
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return result.scalars().all()
return list(result.scalars().all())
+32 -31
View File
@@ -1,8 +1,9 @@
from typing import Any, Optional
from datetime import datetime
from sqlalchemy import Boolean, Column, Integer, String, JSON, select, delete
from sqlalchemy import Boolean, Integer, String, JSON, select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, Base, async_db_query, async_db_update, get_id_column
@@ -13,52 +14,52 @@ class Site(Base):
"""
id = get_id_column()
# 站点名
name = Column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
# 域名Key
domain = Column(String, index=True)
domain: Mapped[Optional[str]] = mapped_column(String, index=True)
# 站点地址
url = Column(String, nullable=False)
url: Mapped[str] = mapped_column(String, nullable=False)
# 站点优先级
pri = Column(Integer, default=1)
pri: Mapped[Optional[int]] = mapped_column(Integer, default=1)
# RSS地址,未启用
rss = Column(String)
rss: Mapped[Optional[str]] = mapped_column(String)
# Cookie
cookie = Column(String)
cookie: Mapped[Optional[str]] = mapped_column(String)
# User-Agent
ua = Column(String)
ua: Mapped[Optional[str]] = mapped_column(String)
# ApiKey
apikey = Column(String)
apikey: Mapped[Optional[str]] = mapped_column(String)
# Token
token = Column(String)
token: Mapped[Optional[str]] = mapped_column(String)
# 是否使用代理 0-否,1-是
proxy = Column(Integer)
proxy: Mapped[Optional[int]] = mapped_column(Integer)
# 过滤规则
filter = Column(String)
filter: Mapped[Optional[str]] = mapped_column(String)
# 是否渲染
render = Column(Integer)
render: Mapped[Optional[int]] = mapped_column(Integer)
# 是否公开站点
public = Column(Integer)
public: Mapped[Optional[int]] = mapped_column(Integer)
# 附加信息
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
# 流控单位周期
limit_interval = Column(Integer, default=0)
limit_interval: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 流控次数
limit_count = Column(Integer, default=0)
limit_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 流控间隔
limit_seconds = Column(Integer, default=0)
limit_seconds: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 超时时间
timeout = Column(Integer, default=15)
timeout: Mapped[Optional[int]] = mapped_column(Integer, default=15)
# 是否启用
is_active = Column(Boolean(), default=True)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean(), default=True)
# 创建时间
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
lst_mod_date: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
@classmethod
@db_query
def get_by_domain(cls, db: Session, domain: str):
return db.query(cls).filter(cls.domain == domain).first()
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@async_db_query
@@ -75,34 +76,34 @@ class Site(Base):
@classmethod
@db_query
def get_actives(cls, db: Session):
return db.query(cls).filter(cls.is_active).all()
return list(db.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
@classmethod
@async_db_query
async def async_get_actives(cls, db: AsyncSession):
result = await db.execute(select(cls).where(cls.is_active))
return result.scalars().all()
result = await db.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
@classmethod
@db_query
def list_order_by_pri(cls, db: Session):
return db.query(cls).order_by(cls.pri).all()
return list(db.execute(select(cls).order_by(cls.pri)).scalars().all())
@classmethod
@async_db_query
async def async_list_order_by_pri(cls, db: AsyncSession):
result = await db.execute(select(cls).order_by(cls.pri))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_domains_by_ids(cls, db: Session, ids: list):
return [r[0] for r in db.query(cls.domain).filter(cls.id.in_(ids)).all()]
return list(db.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
@classmethod
@db_update
def reset(cls, db: Session):
db.query(cls).delete()
db.execute(delete(cls))
@classmethod
@async_db_update
+8 -7
View File
@@ -1,6 +1,7 @@
from sqlalchemy import Column, String, select
from typing import Optional
from sqlalchemy import String, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, Base, get_id_column, async_db_query
@@ -11,18 +12,18 @@ class SiteIcon(Base):
"""
id = get_id_column()
# 站点名称
name = Column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
# 域名Key
domain = Column(String, index=True)
domain: Mapped[Optional[str]] = mapped_column(String, index=True)
# 图标地址
url = Column(String, nullable=False)
url: Mapped[str] = mapped_column(String, nullable=False)
# 图标Base64
base64 = Column(String)
base64: Mapped[Optional[str]] = mapped_column(String)
@classmethod
@db_query
def get_by_domain(cls, db: Session, domain: str):
return db.query(cls).filter(cls.domain == domain).first()
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@async_db_query
+12 -11
View File
@@ -1,8 +1,9 @@
from typing import Any, Optional
from datetime import datetime
from sqlalchemy import Column, Integer, String, JSON, select
from sqlalchemy import Integer, String, JSON, delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, Base, async_db_query
@@ -13,24 +14,24 @@ class SiteStatistic(Base):
"""
id = get_id_column()
# 域名Key
domain = Column(String, index=True)
domain: Mapped[Optional[str]] = mapped_column(String, index=True)
# 成功次数
success = Column(Integer)
success: Mapped[Optional[int]] = mapped_column(Integer)
# 失败次数
fail = Column(Integer)
fail: Mapped[Optional[int]] = mapped_column(Integer)
# 平均耗时 秒
seconds = Column(Integer)
seconds: Mapped[Optional[int]] = mapped_column(Integer)
# 最后一次访问状态 0-成功 1-失败
lst_state = Column(Integer)
lst_state: Mapped[Optional[int]] = mapped_column(Integer)
# 最后访问时间
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
lst_mod_date: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 耗时记录 Json
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
@classmethod
@db_query
def get_by_domain(cls, db: Session, domain: str):
return db.query(cls).filter(cls.domain == domain).first()
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@async_db_query
@@ -41,4 +42,4 @@ class SiteStatistic(Base):
@classmethod
@db_update
def reset(cls, db: Session):
db.query(cls).delete()
db.execute(delete(cls))
+48 -51
View File
@@ -1,11 +1,11 @@
from datetime import datetime
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Column, Integer, String, Float, JSON, Index, func, or_, select
from sqlalchemy import Integer, String, Float, JSON, Index, delete, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, Base, get_id_column, async_db_query
from app.db import Base, async_db_query, db_query, db_update, execute_dml, get_id_column
class SiteUserData(Base):
@@ -14,45 +14,45 @@ class SiteUserData(Base):
"""
id = get_id_column()
# 站点域名
domain = Column(String)
domain: Mapped[Optional[str]] = mapped_column(String)
# 站点名称
name = Column(String)
name: Mapped[Optional[str]] = mapped_column(String)
# 用户名
username = Column(String)
username: Mapped[Optional[str]] = mapped_column(String)
# 用户ID
userid = Column(String)
userid: Mapped[Optional[str]] = mapped_column(String)
# 用户等级
user_level = Column(String)
user_level: Mapped[Optional[str]] = mapped_column(String)
# 加入时间
join_at = Column(String)
join_at: Mapped[Optional[str]] = mapped_column(String)
# 积分
bonus = Column(Float, default=0)
bonus: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 上传量
upload = Column(Float, default=0)
upload: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 下载量
download = Column(Float, default=0)
download: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 分享率
ratio = Column(Float, default=0)
ratio: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 做种数
seeding = Column(Float, default=0)
seeding: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 下载数
leeching = Column(Float, default=0)
leeching: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 做种体积
seeding_size = Column(Float, default=0)
seeding_size: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 下载体积
leeching_size = Column(Float, default=0)
leeching_size: Mapped[Optional[float]] = mapped_column(Float, default=0)
# 做种人数, 种子大小 JSON
seeding_info = Column(JSON, default=dict)
seeding_info: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 未读消息
message_unread = Column(Integer, default=0)
message_unread: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 未读消息内容 JSON
message_unread_contents = Column(JSON, default=list)
message_unread_contents: Mapped[Optional[Any]] = mapped_column(JSON, default=list)
# 错误信息
err_msg = Column(String)
err_msg: Mapped[Optional[str]] = mapped_column(String)
# 更新日期
updated_day = Column(String, default=datetime.now().strftime('%Y-%m-%d'))
updated_day: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime('%Y-%m-%d'))
# 更新时间
updated_time = Column(String, default=datetime.now().strftime('%H:%M:%S'))
updated_time: Mapped[Optional[str]] = mapped_column(String, default=datetime.now().strftime('%H:%M:%S'))
__table_args__ = (
Index('ix_siteuserdata_updated_day_id', 'updated_day', 'id'),
@@ -62,14 +62,13 @@ class SiteUserData(Base):
@classmethod
@db_query
def get_by_domain(cls, db: Session, domain: str, workdate: Optional[str] = None, worktime: Optional[str] = None):
statement = select(cls).where(cls.domain == domain)
if workdate and worktime:
return db.query(cls).filter(cls.domain == domain,
cls.updated_day == workdate,
cls.updated_time == worktime).all()
statement = statement.where(cls.updated_day == workdate,
cls.updated_time == worktime)
elif workdate:
return db.query(cls).filter(cls.domain == domain,
cls.updated_day == workdate).all()
return db.query(cls).filter(cls.domain == domain).all()
statement = statement.where(cls.updated_day == workdate)
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
@@ -80,12 +79,12 @@ class SiteUserData(Base):
elif workdate:
query = query.filter(cls.updated_day == workdate)
result = await db.execute(query)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_by_date(cls, db: Session, date: str):
return db.query(cls).filter(cls.updated_day == date).all()
return list(db.execute(select(cls).where(cls.updated_day == date)).scalars().all())
@classmethod
@db_query
@@ -94,21 +93,23 @@ class SiteUserData(Base):
获取各站点最新一天的数据
"""
subquery = (
db.query(
select(
cls.domain,
func.max(cls.updated_day).label('latest_update_day')
)
.where(or_(cls.err_msg.is_(None), cls.err_msg == ""))
.group_by(cls.domain)
.filter(or_(cls.err_msg.is_(None), cls.err_msg == ""))
.subquery()
)
# 主查询:按 domain 和 updated_day 获取最新的记录
return db.query(cls).join(
subquery,
(cls.domain == subquery.c.domain) &
(cls.updated_day == subquery.c.latest_update_day)
).order_by(cls.updated_time.desc()).all()
return list(db.execute(
select(cls).join(
subquery,
(cls.domain == subquery.c.domain) &
(cls.updated_day == subquery.c.latest_update_day)
).order_by(cls.updated_time.desc())
).scalars().all())
@classmethod
@async_db_query
@@ -133,7 +134,7 @@ class SiteUserData(Base):
(cls.domain == subquery.c.domain) &
(cls.updated_day == subquery.c.latest_update_day)
).order_by(cls.updated_time.desc()))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_update
@@ -146,19 +147,15 @@ class SiteUserData(Base):
"""
分批删除指定日期之前的站点用户快照。
"""
ids = [
row[0]
for row in db.query(cls.id)
.filter(cls.updated_day < before_day)
ids = db.execute(
select(cls.id)
.where(cls.updated_day < before_day)
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
deleted = (
db.query(cls)
.filter(cls.id.in_(ids))
.delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
return deleted
+95 -100
View File
@@ -1,12 +1,12 @@
import time
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Column, Integer, String, Float, JSON, Index, or_, select
from sqlalchemy import Integer, String, Float, JSON, Index, delete, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, Base, async_db_query, async_db_update
from app.db.models.media_identity import media_identity_constraint
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -16,101 +16,101 @@ class Subscribe(Base):
"""
id = get_id_column()
# 标题
name = Column(String, nullable=False, index=True)
name: Mapped[str] = mapped_column(String, nullable=False, index=True)
# 年份
year = Column(String)
year: Mapped[Optional[str]] = mapped_column(String)
# 类型
type = Column(String)
type: Mapped[Optional[str]] = mapped_column(String)
# 搜索关键字
keyword = Column(String)
media_source = Column(String, index=True)
media_id = Column(String, index=True)
keyword: Mapped[Optional[str]] = mapped_column(String)
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 音乐实体类型:recording 单曲、album 专辑
music_type = Column(String)
music_type: Mapped[Optional[str]] = mapped_column(String)
# 专辑预期总曲目数,供整专资源完整性判断
total_tracks = Column(Integer)
total_tracks: Mapped[Optional[int]] = mapped_column(Integer)
# 季号
season = Column(Integer)
season: Mapped[Optional[int]] = mapped_column(Integer)
# 海报
poster = Column(String)
poster: Mapped[Optional[str]] = mapped_column(String)
# 背景图
backdrop = Column(String)
backdrop: Mapped[Optional[str]] = mapped_column(String)
# 评分,float
vote = Column(Float)
vote: Mapped[Optional[float]] = mapped_column(Float)
# 简介
description = Column(String)
description: Mapped[Optional[str]] = mapped_column(String)
# 过滤规则
filter = Column(String)
filter: Mapped[Optional[str]] = mapped_column(String)
# 包含
include = Column(String)
include: Mapped[Optional[str]] = mapped_column(String)
# 排除
exclude = Column(String)
exclude: Mapped[Optional[str]] = mapped_column(String)
# 质量
quality = Column(String)
quality: Mapped[Optional[str]] = mapped_column(String)
# 分辨率
resolution = Column(String)
resolution: Mapped[Optional[str]] = mapped_column(String)
# 特效
effect = Column(String)
effect: Mapped[Optional[str]] = mapped_column(String)
# 音乐音质等级:hires/lossless/lossy,可用正则组合
audio_quality = Column(String)
audio_quality: Mapped[Optional[str]] = mapped_column(String)
# 音频格式,可用正则组合
audio_format = Column(String)
audio_format: Mapped[Optional[str]] = mapped_column(String)
# 最低码率(bps
min_bitrate = Column(Integer)
min_bitrate: Mapped[Optional[int]] = mapped_column(Integer)
# 最低位深(bit
min_bit_depth = Column(Integer)
min_bit_depth: Mapped[Optional[int]] = mapped_column(Integer)
# 最低采样率(Hz
min_sample_rate = Column(Integer)
min_sample_rate: Mapped[Optional[int]] = mapped_column(Integer)
# 总集数
total_episode = Column(Integer)
total_episode: Mapped[Optional[int]] = mapped_column(Integer)
# 开始集数
start_episode = Column(Integer)
start_episode: Mapped[Optional[int]] = mapped_column(Integer)
# 缺失集数
lack_episode = Column(Integer)
lack_episode: Mapped[Optional[int]] = mapped_column(Integer)
# 附加信息
note = Column(JSON)
note: Mapped[Optional[Any]] = mapped_column(JSON)
# 状态:N-新建 R-订阅中 P-待定 S-暂停
state = Column(String, nullable=False, index=True, default='N')
state: Mapped[str] = mapped_column(String, nullable=False, index=True, default='N')
# 最后更新时间
last_update = Column(String)
last_update: Mapped[Optional[str]] = mapped_column(String)
# 创建时间
date = Column(String)
date: Mapped[Optional[str]] = mapped_column(String)
# 订阅用户
username = Column(String, index=True)
username: Mapped[Optional[str]] = mapped_column(String, index=True)
# 订阅站点
sites = Column(JSON, default=list)
sites: Mapped[Optional[Any]] = mapped_column(JSON, default=list)
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
# 是否洗版
best_version = Column(Integer, default=0)
best_version: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 是否只洗全集整包,开启后电视剧洗版不按单集下载
best_version_full = Column(Integer, default=0)
best_version_full: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 当前优先级
current_priority = Column(Integer)
current_priority: Mapped[Optional[int]] = mapped_column(Integer)
# 当前音乐版本格式
current_audio_format = Column(String)
current_audio_format: Mapped[Optional[str]] = mapped_column(String)
# 当前音乐版本码率(bps
current_bitrate = Column(Integer)
current_bitrate: Mapped[Optional[int]] = mapped_column(Integer)
# 当前音乐版本位深(bit
current_bit_depth = Column(Integer)
current_bit_depth: Mapped[Optional[int]] = mapped_column(Integer)
# 当前音乐版本采样率(Hz
current_sample_rate = Column(Integer)
current_sample_rate: Mapped[Optional[int]] = mapped_column(Integer)
# 洗版时已下载剧集的优先级状态,格式:{"1": 90, "2": 100}
episode_priority = Column(JSON)
episode_priority: Mapped[Optional[Any]] = mapped_column(JSON)
# 保存路径
save_path = Column(String)
save_path: Mapped[Optional[str]] = mapped_column(String)
# 是否使用 imdbid 搜索
search_imdbid = Column(Integer, default=0)
search_imdbid: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 是否手动修改过总集数 0否 1是
manual_total_episode = Column(Integer, default=0)
manual_total_episode: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 自定义识别词
custom_words = Column(String)
custom_words: Mapped[Optional[str]] = mapped_column(String)
# 自定义媒体类别
media_category = Column(String)
media_category: Mapped[Optional[str]] = mapped_column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
filter_groups: Mapped[Optional[Any]] = mapped_column(JSON, default=list)
# 选择的剧集组
episode_group = Column(String)
episode_group: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
media_identity_constraint("subscribe"),
@@ -152,11 +152,11 @@ class Subscribe(Base):
)
if condition is None:
return None
query = db.query(cls).filter(condition)
statement = select(cls).where(condition)
if season is not None:
query = query.filter(cls.season == season)
query = query.filter(cls.episode_group == episode_group)
return query.first()
statement = statement.where(cls.season == season)
statement = statement.where(cls.episode_group == episode_group)
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
@@ -197,11 +197,11 @@ class Subscribe(Base):
)
if condition is None:
return None
query = db.query(cls).filter(cls.username == username, condition)
statement = select(cls).where(cls.username == username, condition)
if season is not None:
query = query.filter(cls.season == season)
query = query.filter(cls.episode_group == episode_group)
return query.first()
statement = statement.where(cls.season == season)
statement = statement.where(cls.episode_group == episode_group)
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
@@ -232,11 +232,11 @@ class Subscribe(Base):
@db_query
def get_by_state(cls, db: Session, state: str):
# 如果 state 为空或 None,返回所有订阅
if not state:
return db.query(cls).all()
else:
statement = select(cls)
if state:
# 如果传入的状态不为空,拆分成多个状态
return db.query(cls).filter(cls.state.in_(state.split(','))).all()
statement = statement.where(cls.state.in_(state.split(',')))
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
@@ -249,15 +249,15 @@ class Subscribe(Base):
result = await db.execute(
select(cls).filter(cls.state.in_(state.split(',')))
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_by_title(cls, db: Session, title: str, season: Optional[int] = None):
statement = select(cls).where(cls.name == title)
if season is not None:
return db.query(cls).filter(cls.name == title,
cls.season == season).first()
return db.query(cls).filter(cls.name == title).first()
statement = statement.where(cls.season == season)
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
@@ -286,7 +286,7 @@ class Subscribe(Base):
result = await db.execute(
select(cls).filter(cls.name == title)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
@@ -302,7 +302,7 @@ class Subscribe(Base):
)
if condition is None:
return []
return db.query(cls).filter(condition).all()
return list(db.execute(select(cls).where(condition)).scalars().all())
@classmethod
@async_db_query
@@ -319,7 +319,7 @@ class Subscribe(Base):
if condition is None:
return []
result = await db.execute(select(cls).filter(condition))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
@@ -336,10 +336,10 @@ class Subscribe(Base):
)
if condition is None:
return None
query = db.query(cls).filter(condition, cls.type == type)
statement = select(cls).where(condition, cls.type == type)
if season is not None:
query = query.filter(cls.season == season)
return query.first()
statement = statement.where(cls.season == season)
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
@@ -368,13 +368,14 @@ class Subscribe(Base):
season: Optional[int] = None,
) -> bool:
"""按规范媒体身份删除订阅。"""
query = db.query(type(self)).filter(
type(self).media_source == media_source,
type(self).media_id == str(media_id),
model = type(self)
statement = delete(model).where(
model.media_source == media_source,
model.media_id == str(media_id),
)
if season is not None:
query = query.filter(type(self).season == season)
query.delete(synchronize_session=False)
statement = statement.where(model.season == season)
db.execute(statement, execution_options={"synchronize_session": False})
return True
@async_db_update
@@ -394,20 +395,12 @@ class Subscribe(Base):
@classmethod
@db_query
def list_by_username(cls, db: Session, username: str, state: Optional[str] = None, mtype: Optional[str] = None):
statement = select(cls).where(cls.username == username)
if state:
statement = statement.where(cls.state == state)
if mtype:
if state:
return db.query(cls).filter(cls.state == state,
cls.username == username,
cls.type == mtype).all()
else:
return db.query(cls).filter(cls.username == username,
cls.type == mtype).all()
else:
if state:
return db.query(cls).filter(cls.state == state,
cls.username == username).all()
else:
return db.query(cls).filter(cls.username == username).all()
statement = statement.where(cls.type == mtype)
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
@@ -431,16 +424,18 @@ class Subscribe(Base):
result = await db.execute(
select(cls).filter(cls.username == username)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def list_by_type(cls, db: Session, mtype: str, days: int):
return db.query(cls) \
.filter(cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
).all()
return list(db.execute(
select(cls).where(
cls.type == mtype,
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
)
).scalars().all())
@classmethod
@async_db_query
@@ -452,4 +447,4 @@ class Subscribe(Base):
time.localtime(time.time() - 86400 * int(days)))
)
)
return result.scalars().all()
return list(result.scalars().all())
+64 -62
View File
@@ -1,11 +1,11 @@
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Column, Integer, String, Float, JSON, Index, or_, select
from sqlalchemy import Integer, String, Float, JSON, Index, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, Base, get_id_column, async_db_query
from app.db.models.media_identity import media_identity_constraint
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -15,89 +15,89 @@ class SubscribeHistory(Base):
"""
id = get_id_column()
# 标题
name = Column(String, nullable=False, index=True)
name: Mapped[str] = mapped_column(String, nullable=False, index=True)
# 年份
year = Column(String)
year: Mapped[Optional[str]] = mapped_column(String)
# 类型
type = Column(String)
type: Mapped[Optional[str]] = mapped_column(String)
# 搜索关键字
keyword = Column(String)
media_source = Column(String, index=True)
media_id = Column(String, index=True)
keyword: Mapped[Optional[str]] = mapped_column(String)
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 音乐实体类型:recording 单曲、album 专辑
music_type = Column(String)
music_type: Mapped[Optional[str]] = mapped_column(String)
# 专辑预期总曲目数
total_tracks = Column(Integer)
total_tracks: Mapped[Optional[int]] = mapped_column(Integer)
# 季号
season = Column(Integer)
season: Mapped[Optional[int]] = mapped_column(Integer)
# 海报
poster = Column(String)
poster: Mapped[Optional[str]] = mapped_column(String)
# 背景图
backdrop = Column(String)
backdrop: Mapped[Optional[str]] = mapped_column(String)
# 评分,float
vote = Column(Float)
vote: Mapped[Optional[float]] = mapped_column(Float)
# 简介
description = Column(String)
description: Mapped[Optional[str]] = mapped_column(String)
# 过滤规则
filter = Column(String)
filter: Mapped[Optional[str]] = mapped_column(String)
# 包含
include = Column(String)
include: Mapped[Optional[str]] = mapped_column(String)
# 排除
exclude = Column(String)
exclude: Mapped[Optional[str]] = mapped_column(String)
# 质量
quality = Column(String)
quality: Mapped[Optional[str]] = mapped_column(String)
# 分辨率
resolution = Column(String)
resolution: Mapped[Optional[str]] = mapped_column(String)
# 特效
effect = Column(String)
effect: Mapped[Optional[str]] = mapped_column(String)
# 音乐音质等级:hires/lossless/lossy,可用正则组合
audio_quality = Column(String)
audio_quality: Mapped[Optional[str]] = mapped_column(String)
# 音频格式,可用正则组合
audio_format = Column(String)
audio_format: Mapped[Optional[str]] = mapped_column(String)
# 最低码率(bps
min_bitrate = Column(Integer)
min_bitrate: Mapped[Optional[int]] = mapped_column(Integer)
# 最低位深(bit
min_bit_depth = Column(Integer)
min_bit_depth: Mapped[Optional[int]] = mapped_column(Integer)
# 最低采样率(Hz
min_sample_rate = Column(Integer)
min_sample_rate: Mapped[Optional[int]] = mapped_column(Integer)
# 总集数
total_episode = Column(Integer)
total_episode: Mapped[Optional[int]] = mapped_column(Integer)
# 开始集数
start_episode = Column(Integer)
start_episode: Mapped[Optional[int]] = mapped_column(Integer)
# 订阅完成时间
date = Column(String)
date: Mapped[Optional[str]] = mapped_column(String)
# 订阅用户
username = Column(String)
username: Mapped[Optional[str]] = mapped_column(String)
# 订阅站点
sites = Column(JSON)
sites: Mapped[Optional[Any]] = mapped_column(JSON)
# 是否洗版
best_version = Column(Integer, default=0)
best_version: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 是否只洗全集整包,开启后电视剧洗版不按单集下载
best_version_full = Column(Integer, default=0)
best_version_full: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 完成时的整体优先级
current_priority = Column(Integer)
current_priority: Mapped[Optional[int]] = mapped_column(Integer)
# 完成时的音乐格式
current_audio_format = Column(String)
current_audio_format: Mapped[Optional[str]] = mapped_column(String)
# 完成时的音乐码率(bps
current_bitrate = Column(Integer)
current_bitrate: Mapped[Optional[int]] = mapped_column(Integer)
# 完成时的音乐位深(bit
current_bit_depth = Column(Integer)
current_bit_depth: Mapped[Optional[int]] = mapped_column(Integer)
# 完成时的音乐采样率(Hz
current_sample_rate = Column(Integer)
current_sample_rate: Mapped[Optional[int]] = mapped_column(Integer)
# 洗版时已下载剧集的优先级状态,格式:{"1": 90, "2": 100}
episode_priority = Column(JSON)
episode_priority: Mapped[Optional[Any]] = mapped_column(JSON)
# 保存路径
save_path = Column(String)
save_path: Mapped[Optional[str]] = mapped_column(String)
# 是否使用 imdbid 搜索
search_imdbid = Column(Integer, default=0)
search_imdbid: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 自定义识别词
custom_words = Column(String)
custom_words: Mapped[Optional[str]] = mapped_column(String)
# 自定义媒体类别
media_category = Column(String)
media_category: Mapped[Optional[str]] = mapped_column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
filter_groups: Mapped[Optional[Any]] = mapped_column(JSON, default=list)
# 剧集组
episode_group = Column(String)
episode_group: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
media_identity_constraint("subscribehistory"),
@@ -107,16 +107,18 @@ class SubscribeHistory(Base):
@classmethod
@db_query
def list_by_type(cls, db: Session, mtype: str, page: Optional[int] = 1, count: Optional[int] = 30):
return db.query(cls).filter(
cls.type == mtype
).order_by(
cls.date.desc()
).offset((page - 1) * count).limit(count).all()
def list_by_type(cls, db: Session, mtype: str, page: int = 1, count: int = 30):
return list(db.execute(
select(cls).where(
cls.type == mtype
).order_by(
cls.date.desc()
).offset((page - 1) * count).limit(count)
).scalars().all())
@classmethod
@async_db_query
async def async_list_by_type(cls, db: AsyncSession, mtype: str, page: Optional[int] = 1, count: Optional[int] = 30):
async def async_list_by_type(cls, db: AsyncSession, mtype: str, page: int = 1, count: int = 30):
result = await db.execute(
select(cls).filter(
cls.type == mtype
@@ -124,7 +126,7 @@ class SubscribeHistory(Base):
cls.date.desc()
).offset((page - 1) * count).limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@async_db_query
@@ -133,8 +135,8 @@ class SubscribeHistory(Base):
db: AsyncSession,
mtype: str,
username: str,
page: Optional[int] = 1,
count: Optional[int] = 30
page: int = 1,
count: int = 30
):
"""
按订阅 owner 查询指定类型的历史分页。
@@ -149,7 +151,7 @@ class SubscribeHistory(Base):
cls.date.desc()
).offset((page - 1) * count).limit(count)
)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
def _identity_condition(
@@ -185,11 +187,11 @@ class SubscribeHistory(Base):
)
if condition is None:
return None
query = db.query(cls).filter(condition)
statement = select(cls).where(condition)
if season is not None:
query = query.filter(cls.season == season)
query = query.filter(cls.episode_group == episode_group)
return query.first()
statement = statement.where(cls.season == season)
statement = statement.where(cls.episode_group == episode_group)
return db.execute(statement).scalars().first()
@classmethod
@async_db_query
+6 -5
View File
@@ -1,6 +1,7 @@
from sqlalchemy import Column, String, JSON, select
from typing import Any, Optional
from sqlalchemy import String, JSON, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, Base, async_db_query, get_id_column
@@ -11,14 +12,14 @@ class SystemConfig(Base):
"""
id = get_id_column()
# 主键
key = Column(String, index=True)
key: Mapped[Optional[str]] = mapped_column(String, index=True)
# 值
value = Column(JSON)
value: Mapped[Optional[Any]] = mapped_column(JSON)
@classmethod
@db_query
def get_by_key(cls, db: Session, key: str):
return db.query(cls).filter(cls.key == key).first()
return db.execute(select(cls).where(cls.key == key)).scalars().first()
@classmethod
@async_db_query
+153 -177
View File
@@ -1,14 +1,14 @@
import re
import time
from pathlib import Path
from typing import List, Optional
from typing import Any, List, Optional
from sqlalchemy import Boolean, Column, Index, Integer, JSON, String, func, or_, select
from sqlalchemy import Boolean, Index, Integer, JSON, String, delete, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, Base, async_db_query
from app.db.models.media_identity import media_identity_constraint
from app.db import Base, async_db_query, db_query, db_update, execute_dml, get_id_column
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
@@ -25,64 +25,64 @@ class TransferHistory(Base):
"""
id = get_id_column()
# 源路径
src = Column(String, index=True)
src: Mapped[Optional[str]] = mapped_column(String, index=True)
# 源存储
src_storage = Column(String, nullable=False, default="local")
src_storage: Mapped[str] = mapped_column(String, nullable=False, default="local")
# 源文件项
src_fileitem = Column(JSON, default=dict)
src_fileitem: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 目标路径
dest = Column(String)
dest: Mapped[Optional[str]] = mapped_column(String)
# 目标存储
dest_storage = Column(String)
dest_storage: Mapped[Optional[str]] = mapped_column(String)
# 目标文件项
dest_fileitem = Column(JSON, default=dict)
dest_fileitem: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 转移模式 move/copy/link...
mode = Column(String)
mode: Mapped[Optional[str]] = mapped_column(String)
# 类型 电影/电视剧
type = Column(String)
type: Mapped[Optional[str]] = mapped_column(String)
# 二级分类
category = Column(String)
category: Mapped[Optional[str]] = mapped_column(String)
# 标题
title = Column(String, index=True)
title: Mapped[Optional[str]] = mapped_column(String, index=True)
# 年份
year = Column(String)
year: Mapped[Optional[str]] = mapped_column(String)
# 媒体数据源与原生ID
media_source = Column(String, index=True)
media_id = Column(String, index=True)
media_source: Mapped[Optional[str]] = mapped_column(String, index=True)
media_id: Mapped[Optional[str]] = mapped_column(String, index=True)
# 音乐实体类型:recording 单曲、album 专辑
music_type = Column(String)
music_type: Mapped[Optional[str]] = mapped_column(String)
# 专辑预期总曲目数
total_tracks = Column(Integer)
total_tracks: Mapped[Optional[int]] = mapped_column(Integer)
# 实际音频格式
audio_format = Column(String)
audio_format: Mapped[Optional[str]] = mapped_column(String)
# 是否无损音频
audio_lossless = Column(Boolean)
audio_lossless: Mapped[Optional[bool]] = mapped_column(Boolean)
# 实际位深(bit
bit_depth = Column(Integer)
bit_depth: Mapped[Optional[int]] = mapped_column(Integer)
# 实际采样率(Hz
sample_rate = Column(Integer)
sample_rate: Mapped[Optional[int]] = mapped_column(Integer)
# 实际码率(bps
bitrate = Column(Integer)
bitrate: Mapped[Optional[int]] = mapped_column(Integer)
# Sxx
seasons = Column(String)
seasons: Mapped[Optional[str]] = mapped_column(String)
# Exx
episodes = Column(String)
episodes: Mapped[Optional[str]] = mapped_column(String)
# 海报
image = Column(String)
image: Mapped[Optional[str]] = mapped_column(String)
# 下载器
downloader = Column(String)
downloader: Mapped[Optional[str]] = mapped_column(String)
# 下载器hash
download_hash = Column(String, index=True)
download_hash: Mapped[Optional[str]] = mapped_column(String, index=True)
# 转移成功状态
status = Column(Boolean(), default=True)
status: Mapped[Optional[bool]] = mapped_column(Boolean(), default=True)
# 转移失败信息
errmsg = Column(String)
errmsg: Mapped[Optional[str]] = mapped_column(String)
# 时间
date = Column(String)
date: Mapped[Optional[str]] = mapped_column(String)
# 文件清单,以JSON存储
files = Column(JSON, default=list)
files: Mapped[Optional[Any]] = mapped_column(JSON, default=list)
# 剧集组
episode_group = Column(String)
episode_group: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
media_identity_constraint("transferhistory"),
@@ -94,8 +94,8 @@ class TransferHistory(Base):
@classmethod
@db_query
def list_by_title(cls, db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
status: bool = None, wildcard: bool = False):
def list_by_title(cls, db: Session, title: str, page: int = 1, count: int = 30,
status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
_text_like(cls.title, title, wildcard=True),
@@ -108,21 +108,21 @@ class TransferHistory(Base):
_text_like(cls.src, f'%{title}%'),
_text_like(cls.dest, f'%{title}%'),
)
query = db.query(cls).filter(text_filter)
statement = select(cls).where(text_filter)
if status is not None:
query = query.filter(cls.status == status)
query = query.order_by(cls.date.desc())
statement = statement.where(cls.status == status)
statement = statement.order_by(cls.date.desc())
# 当count为负数时,不限制页数查询所有
if count >= 0:
query = query.offset((page - 1) * count).limit(count)
statement = statement.offset((page - 1) * count).limit(count)
return query.all()
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
async def async_list_by_title(cls, db: AsyncSession, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
status: bool = None, wildcard: bool = False):
async def async_list_by_title(cls, db: AsyncSession, title: str, page: int = 1, count: int = 30,
status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
_text_like(cls.title, title, wildcard=True),
@@ -145,32 +145,26 @@ class TransferHistory(Base):
query = query.offset((page - 1) * count).limit(count)
result = await db.execute(query)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30, status: bool = None):
def list_by_page(cls, db: Session, page: int = 1, count: int = 30, status: Optional[bool] = None):
statement = select(cls)
if status is not None:
query = db.query(cls).filter(
cls.status == status
).order_by(
cls.date.desc()
)
else:
query = db.query(cls).order_by(
cls.date.desc()
)
statement = statement.where(cls.status == status)
statement = statement.order_by(cls.date.desc())
# 当count为负数时,不限制页数查询所有
if count >= 0:
query = query.offset((page - 1) * count).limit(count)
return query.all()
statement = statement.offset((page - 1) * count).limit(count)
return list(db.execute(statement).scalars().all())
@classmethod
@async_db_query
async def async_list_by_page(cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30,
status: bool = None):
async def async_list_by_page(cls, db: AsyncSession, page: int = 1, count: int = 30,
status: Optional[bool] = None):
if status is not None:
query = select(cls).filter(
cls.status == status
@@ -187,12 +181,14 @@ class TransferHistory(Base):
query = query.offset((page - 1) * count).limit(count)
result = await db.execute(query)
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_by_hash(cls, db: Session, download_hash: str):
return db.query(cls).filter(cls.download_hash == download_hash).first()
return db.execute(
select(cls).where(cls.download_hash == download_hash)
).scalars().first()
@classmethod
@db_query
@@ -207,11 +203,12 @@ class TransferHistory(Base):
:param storage: 源存储类型
:return: 命中的整理记录,未命中时返回 None
"""
statement = select(cls).where(cls.src == src)
if storage:
query = db.query(cls).filter(cls.src == src, cls.src_storage == storage)
else:
query = db.query(cls).filter(cls.src == src)
return query.order_by(cls.id.desc()).first()
statement = statement.where(cls.src_storage == storage)
return db.execute(
statement.order_by(cls.id.desc())
).scalars().first()
@classmethod
@db_query
@@ -228,10 +225,12 @@ class TransferHistory(Base):
:param storage: 源存储类型
:return: 命中的成功整理记录,未命中时返回 None
"""
query = db.query(cls).filter(cls.src == src, cls.status.is_(True))
statement = select(cls).where(cls.src == src, cls.status.is_(True))
if storage:
query = query.filter(cls.src_storage == storage)
return query.order_by(cls.id.desc()).first()
statement = statement.where(cls.src_storage == storage)
return db.execute(
statement.order_by(cls.id.desc())
).scalars().first()
@classmethod
@db_query
@@ -246,10 +245,12 @@ class TransferHistory(Base):
:param storage: 目标存储类型
:return: 命中的整理记录,未命中时返回 None
"""
query = db.query(cls).filter(cls.dest == dest)
statement = select(cls).where(cls.dest == dest)
if storage:
query = query.filter(cls.dest_storage == storage)
return query.order_by(cls.id.desc()).first()
statement = statement.where(cls.dest_storage == storage)
return db.execute(
statement.order_by(cls.id.desc())
).scalars().first()
@classmethod
@db_query
@@ -272,24 +273,24 @@ class TransferHistory(Base):
normalized_src = (
Path(str(src).replace("\\", "/")).as_posix().rstrip("/") or "/"
)
query = db.query(cls).filter(cls.status.is_(True))
statement = select(cls).where(cls.status.is_(True))
if recursive:
escaped_src = (
normalized_src.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
query = query.filter(
statement = statement.where(
or_(
cls.src == normalized_src,
cls.src.like(f"{escaped_src.rstrip('/')}/%", escape="\\"),
)
)
else:
query = query.filter(cls.src == normalized_src)
statement = statement.where(cls.src == normalized_src)
if storage:
query = query.filter(cls.src_storage == storage)
return query.all()
statement = statement.where(cls.src_storage == storage)
return list(db.execute(statement).scalars().all())
@classmethod
@db_query
@@ -312,7 +313,7 @@ class TransferHistory(Base):
normalized_dest = (
Path(str(dest).replace("\\", "/")).as_posix().rstrip("/") or "/"
)
query = db.query(cls).filter(
statement = select(cls).where(
cls.status.is_(True),
cls.mode.contains("move"),
)
@@ -322,34 +323,41 @@ class TransferHistory(Base):
.replace("%", "\\%")
.replace("_", "\\_")
)
query = query.filter(
statement = statement.where(
or_(
cls.dest == normalized_dest,
cls.dest.like(f"{escaped_dest.rstrip('/')}/%", escape="\\"),
)
)
else:
query = query.filter(cls.dest == normalized_dest)
statement = statement.where(cls.dest == normalized_dest)
if storage:
query = query.filter(cls.dest_storage == storage)
return query.all()
statement = statement.where(cls.dest_storage == storage)
return list(db.execute(statement).scalars().all())
@classmethod
@db_query
def list_by_hash(cls, db: Session, download_hash: str):
return db.query(cls).filter(cls.download_hash == download_hash).all()
return list(db.execute(
select(cls).where(cls.download_hash == download_hash)
).scalars().all())
@classmethod
@db_query
def statistic(cls, db: Session, days: Optional[int] = 7):
def statistic(cls, db: Session, days: int = 7):
"""
统计最近days天的下载历史数量,按日期分组返回每日数量
"""
sub_query = db.query(func.substr(cls.date, 1, 10).label('date'),
cls.id.label('id')).filter(
sub_query = select(
func.substr(cls.date, 1, 10).label('date'),
cls.id.label('id')
).where(
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * days))).subquery()
return db.query(sub_query.c.date, func.count(sub_query.c.id)).group_by(sub_query.c.date).all()
time.localtime(time.time() - 86400 * days))
).subquery()
return list(db.execute(
select(sub_query.c.date, func.count(sub_query.c.id)).group_by(sub_query.c.date)
).all())
@classmethod
@db_query
@@ -361,11 +369,11 @@ class TransferHistory(Base):
缺少集数时按单条成功整理记录计数;音乐按曲目身份去重,整专记录不能只按专辑 ID 合并。
"""
month_prefix = time.strftime("%Y-%m-", time.localtime())
histories = db.query(cls).filter(
histories = db.execute(select(cls).where(
cls.status.is_(True),
cls.date.like(f"{month_prefix}%"),
cls.type.in_([MediaType.MOVIE.value, MediaType.TV.value, MediaType.MUSIC.value]),
).all()
)).scalars().all()
movie_identities = set()
tv_identities = set()
episode_count = 0
@@ -419,7 +427,7 @@ class TransferHistory(Base):
@classmethod
@async_db_query
async def async_statistic(cls, db: AsyncSession, days: Optional[int] = 7):
async def async_statistic(cls, db: AsyncSession, days: int = 7):
"""
统计最近days天的下载历史数量,按日期分组返回每日数量
"""
@@ -434,15 +442,15 @@ class TransferHistory(Base):
@classmethod
@db_query
def count(cls, db: Session, status: bool = None):
def count(cls, db: Session, status: Optional[bool] = None):
statement = select(func.count(cls.id))
if status is not None:
return db.query(func.count(cls.id)).filter(cls.status == status).first()[0]
else:
return db.query(func.count(cls.id)).first()[0]
statement = statement.where(cls.status == status)
return db.execute(statement).scalar()
@classmethod
@async_db_query
async def async_count(cls, db: AsyncSession, status: bool = None):
async def async_count(cls, db: AsyncSession, status: Optional[bool] = None):
if status is not None:
result = await db.execute(
select(func.count(cls.id)).filter(cls.status == status)
@@ -455,7 +463,7 @@ class TransferHistory(Base):
@classmethod
@db_query
def count_by_title(cls, db: Session, title: str, status: bool = None, wildcard: bool = False):
def count_by_title(cls, db: Session, title: str, status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
_text_like(cls.title, title, wildcard=True),
@@ -468,14 +476,14 @@ class TransferHistory(Base):
_text_like(cls.src, f'%{title}%'),
_text_like(cls.dest, f'%{title}%'),
)
query = db.query(func.count(cls.id)).filter(text_filter)
statement = select(func.count(cls.id)).where(text_filter)
if status is not None:
query = query.filter(cls.status == status)
return query.first()[0]
statement = statement.where(cls.status == status)
return db.execute(statement).scalar()
@classmethod
@async_db_query
async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None, wildcard: bool = False):
async def async_count_by_title(cls, db: AsyncSession, title: str, status: Optional[bool] = None, wildcard: bool = False):
if wildcard:
text_filter = or_(
_text_like(cls.title, title, wildcard=True),
@@ -506,63 +514,31 @@ class TransferHistory(Base):
按媒体身份、季集或标题年份查询整理记录。
"""
if media_source and media_id and mtype:
# 电视剧某季某集
if season is not None and episode:
return db.query(cls).filter(cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype,
cls.seasons == season,
cls.episodes == episode,
cls.dest == dest).all()
# 电视剧某季
elif season is not None:
return db.query(cls).filter(cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype,
cls.seasons == season).all()
else:
if dest:
# 电影
return db.query(cls).filter(cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype,
cls.dest == dest).all()
else:
# 电视剧所有季集
return db.query(cls).filter(cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype).all()
# 标题 + 年份
statement = select(cls).where(cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype)
elif title and year:
# 电视剧某季某集
if season is not None and episode:
return db.query(cls).filter(cls.title == title,
cls.year == year,
cls.seasons == season,
cls.episodes == episode,
cls.dest == dest).all()
# 电视剧某季
elif season is not None:
return db.query(cls).filter(cls.title == title,
cls.year == year,
cls.seasons == season).all()
else:
if dest:
# 电影
return db.query(cls).filter(cls.title == title,
cls.year == year,
cls.dest == dest).all()
else:
# 电视剧所有季集
return db.query(cls).filter(cls.title == title,
cls.year == year).all()
# 类型 + 转移路径(媒体服务器 webhook 缺少远端身份场景)
statement = select(cls).where(cls.title == title,
cls.year == year)
elif mtype and season is not None and dest:
# 类型 + 转移路径(媒体服务器 webhook 缺少远端身份场景)
return list(db.execute(select(cls).where(cls.type == mtype,
cls.seasons == season,
cls.dest.like(f"{dest}%"))).scalars().all())
else:
return []
if season is not None and episode:
# 电视剧某季某集:目标路径同样参与匹配,dest 为空即匹配空目标
statement = statement.where(cls.seasons == season,
cls.episodes == episode,
cls.dest == dest)
elif season is not None:
# 电视剧某季
return db.query(cls).filter(cls.type == mtype,
cls.seasons == season,
cls.dest.like(f"{dest}%")).all()
return []
statement = statement.where(cls.seasons == season)
elif dest:
# 电影:没有季集,用目标路径区分不同版本
statement = statement.where(cls.dest == dest)
return list(db.execute(statement).scalars().all())
@classmethod
@db_query
@@ -571,19 +547,17 @@ class TransferHistory(Base):
mtype: Optional[str] = None,
):
"""按规范媒体身份和类型查询整理记录。"""
return db.query(cls).filter(
return db.execute(select(cls).where(
cls.media_source == str(media_source),
cls.media_id == str(media_id),
cls.type == mtype,
).first()
)).scalars().first()
@classmethod
@db_update
def update_download_hash(cls, db: Session, historyid: Optional[int] = None, download_hash: Optional[str] = None):
db.query(cls).filter(cls.id == historyid).update(
{
"download_hash": download_hash
}
db.execute(
update(cls).where(cls.id == historyid).values(download_hash=download_hash)
)
@classmethod
@@ -602,10 +576,13 @@ class TransferHistory(Base):
src_storage = kwargs.get("src_storage") or "local"
kwargs["src_storage"] = src_storage
if src:
db.query(cls).filter(
cls.src == src,
cls.src_storage == src_storage,
).delete(synchronize_session=False)
db.execute(
delete(cls).where(
cls.src == src,
cls.src_storage == src_storage,
),
execution_options={"synchronize_session": False},
)
history = cls(**kwargs)
db.add(history)
db.flush()
@@ -617,7 +594,9 @@ class TransferHistory(Base):
"""
查询某时间之后的转移历史
"""
return db.query(cls).filter(cls.date > date).order_by(cls.id.desc()).all()
return list(db.execute(
select(cls).where(cls.date > date).order_by(cls.id.desc())
).scalars().all())
@classmethod
@db_update
@@ -630,18 +609,15 @@ class TransferHistory(Base):
"""
分批删除指定时间之前的整理历史。
"""
ids = [
row[0]
for row in db.query(cls.id)
.filter(cls.date < before_time)
ids = db.execute(
select(cls.id)
.where(cls.date < before_time)
.order_by(cls.id.asc())
.limit(limit)
.all()
]
).scalars().all()
if not ids:
return 0
return (
db.query(cls)
.filter(cls.id.in_(ids))
.delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.id.in_(ids)),
execution_options={"synchronize_session": False},
)
+20 -17
View File
@@ -1,9 +1,9 @@
from typing import List, Optional
from sqlalchemy import Column, Index, String
from sqlalchemy.orm import Session
from sqlalchemy import Index, String, delete, select
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, db_query, db_update, get_id_column
from app.db import Base, db_query, db_update, execute_dml, get_id_column
class TransferPending(Base):
@@ -22,11 +22,11 @@ class TransferPending(Base):
id = get_id_column()
# 存储
storage = Column(String, nullable=False)
storage: Mapped[str] = mapped_column(String, nullable=False)
# 源文件路径
src_path = Column(String, nullable=False)
src_path: Mapped[str] = mapped_column(String, nullable=False)
# 登记时间
created_at = Column(String)
created_at: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
# 同一个文件重复入队只保留一条,回放时不会重复送入整理链
@@ -47,9 +47,9 @@ class TransferPending(Base):
"""
if not storage or not src_path:
return None
pending = db.query(cls).filter(
cls.storage == storage, cls.src_path == src_path
).first()
pending = db.execute(
select(cls).where(cls.storage == storage, cls.src_path == src_path)
).scalars().first()
if pending:
return pending
pending = cls(storage=storage, src_path=src_path, created_at=now_time)
@@ -68,9 +68,10 @@ class TransferPending(Base):
"""
if not storage or not src_path:
return 0
return db.query(cls).filter(
cls.storage == storage, cls.src_path == src_path
).delete(synchronize_session=False)
return execute_dml(
db, delete(cls).where(cls.storage == storage, cls.src_path == src_path),
execution_options={"synchronize_session": False},
)
@classmethod
@db_query
@@ -84,12 +85,11 @@ class TransferPending(Base):
:param limit: 单次回放上限
:return: 待整理登记列表
"""
return (
db.query(cls)
return list(db.execute(
select(cls)
.order_by(cls.created_at.asc(), cls.id.asc())
.limit(limit)
.all()
)
).scalars().all())
@classmethod
@db_update
@@ -99,4 +99,7 @@ class TransferPending(Base):
:param db: 数据库会话
:return: 删除的记录数
"""
return db.query(cls).delete(synchronize_session=False)
return execute_dml(
db, delete(cls),
execution_options={"synchronize_session": False},
)
+15 -14
View File
@@ -1,6 +1,7 @@
from sqlalchemy import Boolean, Column, JSON, String, select
from typing import Any, Optional
from sqlalchemy import Boolean, JSON, String, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import Base, db_query, db_update, async_db_query, async_db_update, get_id_column
@@ -12,30 +13,30 @@ class User(Base):
# ID
id = get_id_column()
# 用户名,唯一值
name = Column(String, index=True, nullable=False)
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
# 邮箱
email = Column(String)
email: Mapped[Optional[str]] = mapped_column(String)
# 加密后密码
hashed_password = Column(String)
hashed_password: Mapped[Optional[str]] = mapped_column(String)
# 是否启用
is_active = Column(Boolean(), default=True)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean(), default=True)
# 是否管理员
is_superuser = Column(Boolean(), default=False)
is_superuser: Mapped[Optional[bool]] = mapped_column(Boolean(), default=False)
# 头像
avatar = Column(String)
avatar: Mapped[Optional[str]] = mapped_column(String)
# 是否启用otp二次验证
is_otp = Column(Boolean(), default=False)
is_otp: Mapped[Optional[bool]] = mapped_column(Boolean(), default=False)
# otp秘钥
otp_secret = Column(String, default=None)
otp_secret: Mapped[Optional[str]] = mapped_column(String, default=None)
# 用户权限 json
permissions = Column(JSON, default=dict)
permissions: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 用户个性化设置 json
settings = Column(JSON, default=dict)
settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
@classmethod
@db_query
def get_by_name(cls, db: Session, name: str):
return db.query(cls).filter(cls.name == name).first()
return db.execute(select(cls).where(cls.name == name)).scalars().first()
@classmethod
@async_db_query
@@ -48,7 +49,7 @@ class User(Base):
@classmethod
@db_query
def get_by_id(cls, db: Session, user_id: int):
return db.query(cls).filter(cls.id == user_id).first()
return db.execute(select(cls).where(cls.id == user_id)).scalars().first()
@classmethod
@async_db_query
+9 -9
View File
@@ -1,5 +1,6 @@
from sqlalchemy import Column, String, UniqueConstraint, JSON
from sqlalchemy.orm import Session
from typing import Any, Optional
from sqlalchemy import String, UniqueConstraint, JSON, select
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db import db_query, db_update, get_id_column, Base
@@ -10,11 +11,11 @@ class UserConfig(Base):
"""
id = get_id_column()
# 用户名
username = Column(String)
username: Mapped[Optional[str]] = mapped_column(String)
# 配置键
key = Column(String)
key: Mapped[Optional[str]] = mapped_column(String)
# 值
value = Column(JSON)
value: Mapped[Optional[Any]] = mapped_column(JSON)
__table_args__ = (
# 用户名和配置键联合唯一
@@ -24,10 +25,9 @@ class UserConfig(Base):
@classmethod
@db_query
def get_by_key(cls, db: Session, username: str, key: str):
return db.query(cls) \
.filter(cls.username == username) \
.filter(cls.key == key) \
.first()
return db.execute(
select(cls).where(cls.username == username, cls.key == key)
).scalars().first()
@db_update
def delete_by_key(self, db: Session, username: str, key: str):
+56 -70
View File
@@ -1,8 +1,9 @@
from datetime import datetime
from builtins import list as builtin_list
from typing import Optional
from typing import Any, Optional
from sqlalchemy import Column, Integer, JSON, String, Index, and_, or_, select
from sqlalchemy import Integer, JSON, String, Index, and_, or_, select, update
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import Base, db_query, get_id_column, db_update, async_db_query, async_db_update
@@ -15,71 +16,60 @@ class Workflow(Base):
# ID
id = get_id_column()
# 名称
name = Column(String, index=True, nullable=False)
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
# 描述
description = Column(String)
description: Mapped[Optional[str]] = mapped_column(String)
# 定时器
timer = Column(String)
timer: Mapped[Optional[str]] = mapped_column(String)
# 触发类型:timer-定时触发 event-事件触发 manual-手动触发
trigger_type = Column(String, default='timer')
trigger_type: Mapped[Optional[str]] = mapped_column(String, default='timer')
# 事件类型(当trigger_type为event时使用)
event_type = Column(String)
event_type: Mapped[Optional[str]] = mapped_column(String)
# 事件条件(JSON格式,用于过滤事件)
event_conditions = Column(JSON, default=dict)
event_conditions: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 状态:W-等待 R-运行中 P-暂停 S-成功 F-失败
state = Column(String, nullable=False, index=True, default='W')
state: Mapped[str] = mapped_column(String, nullable=False, index=True, default='W')
# 已执行动作(,分隔)
current_action = Column(String)
current_action: Mapped[Optional[str]] = mapped_column(String)
# 任务执行结果
result = Column(String)
result: Mapped[Optional[str]] = mapped_column(String)
# 已执行次数
run_count = Column(Integer, default=0)
run_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
# 任务列表
actions = Column(JSON, default=builtin_list)
actions: Mapped[Optional[Any]] = mapped_column(JSON, default=builtin_list)
# 任务流
flows = Column(JSON, default=builtin_list)
flows: Mapped[Optional[Any]] = mapped_column(JSON, default=builtin_list)
# 执行上下文
context = Column(JSON, default=dict)
context: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 执行配置
execution_config = Column(JSON, default=dict)
execution_config: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 结构化执行状态
execution_state = Column(JSON, default=dict)
execution_state: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
# 创建时间
add_time = Column(String, default=lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
add_time: Mapped[Optional[str]] = mapped_column(String, default=lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
# 最后执行时间
last_time = Column(String)
last_time: Mapped[Optional[str]] = mapped_column(String)
__table_args__ = (
Index('ix_workflow_trigger_type_state', 'trigger_type', 'state'),
)
@classmethod
@db_query
def list(cls, db):
return db.query(cls).all()
@classmethod
@async_db_query
async def async_list(cls, db: AsyncSession):
result = await db.execute(select(cls))
return result.scalars().all()
@classmethod
@db_query
def get_enabled_workflows(cls, db):
return db.query(cls).filter(cls.state != 'P').all()
return list(db.execute(select(cls).where(cls.state != 'P')).scalars().all())
@classmethod
@async_db_query
async def async_get_enabled_workflows(cls, db: AsyncSession):
result = await db.execute(select(cls).where(cls.state != 'P'))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_timer_triggered_workflows(cls, db):
"""获取定时触发的工作流"""
return db.query(cls).filter(
return list(db.execute(select(cls).where(
and_(
or_(
cls.trigger_type == 'timer',
@@ -87,7 +77,7 @@ class Workflow(Base):
),
cls.state != 'P'
)
).all()
)).scalars().all())
@classmethod
@async_db_query
@@ -102,18 +92,18 @@ class Workflow(Base):
cls.state != 'P'
)
))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_event_triggered_workflows(cls, db):
"""获取事件触发的工作流"""
return db.query(cls).filter(
return list(db.execute(select(cls).where(
and_(
cls.trigger_type == 'event',
cls.state != 'P'
)
).all()
)).scalars().all())
@classmethod
@async_db_query
@@ -125,12 +115,12 @@ class Workflow(Base):
cls.state != 'P'
)
))
return result.scalars().all()
return list(result.scalars().all())
@classmethod
@db_query
def get_by_name(cls, db, name: str):
return db.query(cls).filter(cls.name == name).first()
return db.execute(select(cls).where(cls.name == name)).scalars().first()
@classmethod
@async_db_query
@@ -141,45 +131,42 @@ class Workflow(Base):
@classmethod
@db_update
def update_state(cls, db, wid: int, state: str):
db.query(cls).filter(cls.id == wid).update({"state": state})
db.execute(update(cls).where(cls.id == wid).values(state=state))
return True
@classmethod
@async_db_update
async def async_update_state(cls, db: AsyncSession, wid: int, state: str):
from sqlalchemy import update
await db.execute(update(cls).where(cls.id == wid).values(state=state))
return True
@classmethod
@db_update
def start(cls, db, wid: int):
db.query(cls).filter(cls.id == wid).update({
"state": 'R'
})
db.execute(update(cls).where(cls.id == wid).values(state='R'))
return True
@classmethod
@async_db_update
async def async_start(cls, db: AsyncSession, wid: int):
from sqlalchemy import update
await db.execute(update(cls).where(cls.id == wid).values(state='R'))
return True
@classmethod
@db_update
def fail(cls, db, wid: int, result: str):
db.query(cls).filter(and_(cls.id == wid, cls.state != "P")).update({
"state": 'F',
"result": result,
"last_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
db.execute(update(cls).where(
and_(cls.id == wid, cls.state != "P")
).values(
state='F',
result=result,
last_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
))
return True
@classmethod
@async_db_update
async def async_fail(cls, db: AsyncSession, wid: int, result: str):
from sqlalchemy import update
await db.execute(update(cls).where(
and_(cls.id == wid, cls.state != "P")
).values(
@@ -192,18 +179,19 @@ class Workflow(Base):
@classmethod
@db_update
def success(cls, db, wid: int, result: Optional[str] = None):
db.query(cls).filter(and_(cls.id == wid, cls.state != "P")).update({
"state": 'S',
"result": result,
"run_count": cls.run_count + 1,
"last_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
db.execute(update(cls).where(
and_(cls.id == wid, cls.state != "P")
).values(
state='S',
result=result,
run_count=cls.run_count + 1,
last_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
))
return True
@classmethod
@async_db_update
async def async_success(cls, db: AsyncSession, wid: int, result: Optional[str] = None):
from sqlalchemy import update
await db.execute(update(cls).where(
and_(cls.id == wid, cls.state != "P")
).values(
@@ -217,20 +205,19 @@ class Workflow(Base):
@classmethod
@db_update
def reset(cls, db, wid: int, reset_count: Optional[bool] = False):
db.query(cls).filter(cls.id == wid).update({
"state": 'W',
"result": None,
"current_action": None,
"context": {},
"execution_state": {},
"run_count": 0 if reset_count else cls.run_count,
})
db.execute(update(cls).where(cls.id == wid).values(
state='W',
result=None,
current_action=None,
context={},
execution_state={},
run_count=0 if reset_count else cls.run_count,
))
return True
@classmethod
@async_db_update
async def async_reset(cls, db: AsyncSession, wid: int, reset_count: Optional[bool] = False):
from sqlalchemy import update
await db.execute(update(cls).where(cls.id == wid).values(
state='W',
result=None,
@@ -245,7 +232,7 @@ class Workflow(Base):
@db_update
def update_current_action(cls, db, wid: int, action_id: str, context: dict,
execution_state: Optional[dict] = None):
workflow = db.query(cls).filter(cls.id == wid).first()
workflow = db.execute(select(cls).where(cls.id == wid)).scalars().first()
current_actions = []
if workflow and workflow.current_action:
current_actions = [item for item in workflow.current_action.split(",") if item]
@@ -257,14 +244,13 @@ class Workflow(Base):
}
if execution_state is not None:
update_values["execution_state"] = execution_state
db.query(cls).filter(cls.id == wid).update(update_values)
db.execute(update(cls).where(cls.id == wid).values(**update_values))
return True
@classmethod
@async_db_update
async def async_update_current_action(cls, db: AsyncSession, wid: int, action_id: str, context: dict,
execution_state: Optional[dict] = None):
from sqlalchemy import update
# 先获取当前current_action
result = await db.execute(select(cls.current_action).where(cls.id == wid))
current_action = result.scalar()