mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
refactor(db): 修复异步连接池无界增长,并完成 SQLAlchemy 2.0 迁移与分层归位 (#6320)
This commit is contained in:
@@ -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 = (
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user