mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: make model sessions explicit
This commit is contained in:
+24
-42
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ORM 基类与数据访问基类。
|
||||
|
||||
Base 提供声明式基类与兼容行为(字典转换、旧增删改查便利方法);
|
||||
Base 提供声明式基类与显式会话增删改查原语;
|
||||
DbOper 是各业务 Oper 的基类,持有一个可注入的会话。
|
||||
"""
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -12,12 +12,6 @@ from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence,
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column
|
||||
|
||||
from app.db.decorators import (
|
||||
legacy_async_db_query,
|
||||
legacy_async_db_update,
|
||||
legacy_db_query,
|
||||
legacy_db_update,
|
||||
)
|
||||
from app.db.uow import run_async_transaction, run_sync_transaction
|
||||
from app.runtime.config import settings
|
||||
|
||||
@@ -70,98 +64,87 @@ class Base(DeclarativeBase): # type: ignore[misc] # SQLAlchemy 无 py.typed
|
||||
继承本类的模型一律使用 mapped_column() + Mapped[] 注解;确需非映射的类级属性时
|
||||
用 ClassVar 显式声明,而不是把这个标志加回来。
|
||||
|
||||
create/get/update/delete/list/truncate 及其异步版本仅保留旧插件 ABI。宿主新代码应由
|
||||
Application Command 定义事务边界,经显式 Session 调用 Oper,不得新增对这些方法的依赖。
|
||||
create/get/update/delete/list/truncate 及其异步版本都是显式会话原语:只在调用方
|
||||
Session 中暂存或查询,不自行创建、提交、回滚或关闭事务。宿主业务代码应通过 Oper
|
||||
或 Application Command 使用这些能力,插件不得直接依赖宿主模型。
|
||||
"""
|
||||
|
||||
# 由 get_id_column() 在各模型中提供实际的列定义,这里只声明类型供 IDE 使用
|
||||
id: Mapped[int]
|
||||
|
||||
@legacy_db_update
|
||||
def create(self, db: Session) -> None:
|
||||
"""兼容旧插件调用:新增当前模型并提交。"""
|
||||
"""在调用方同步事务中暂存当前模型。"""
|
||||
db.add(self)
|
||||
|
||||
@legacy_async_db_update
|
||||
async def async_create(self, db: AsyncSession) -> Self:
|
||||
"""兼容旧插件调用:异步新增当前模型、刷新主键并提交。"""
|
||||
"""在调用方异步事务中暂存当前模型并刷新主键。"""
|
||||
db.add(self)
|
||||
await db.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get(cls, db: Session, rid: int) -> Optional[Self]:
|
||||
"""兼容旧插件调用:按主键查询当前模型。"""
|
||||
"""在调用方同步会话中按主键查询当前模型。"""
|
||||
return cast(
|
||||
Optional[Self],
|
||||
db.execute(select(cls).where(and_(cls.id == rid))).scalars().first(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get(cls, db: AsyncSession, rid: int) -> Optional[Self]:
|
||||
"""兼容旧插件调用:异步按主键查询当前模型。"""
|
||||
"""在调用方异步会话中按主键查询当前模型。"""
|
||||
result = await db.execute(select(cls).where(and_(cls.id == rid)))
|
||||
return cast(Optional[Self], result.scalars().first())
|
||||
|
||||
@legacy_db_update
|
||||
def update(self, db: Session, payload: dict[str, Any]) -> None:
|
||||
"""兼容旧插件调用:更新当前模型字段并提交。"""
|
||||
"""在调用方同步事务中更新当前模型字段。"""
|
||||
for key, value in payload.items():
|
||||
setattr(self, key, value)
|
||||
if inspect(self).detached:
|
||||
db.add(self)
|
||||
|
||||
@legacy_async_db_update
|
||||
async def async_update(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
payload: dict[str, Any],
|
||||
) -> None:
|
||||
"""兼容旧插件调用:异步更新当前模型字段并提交。"""
|
||||
"""在调用方异步事务中更新当前模型字段。"""
|
||||
for key, value in payload.items():
|
||||
setattr(self, key, value)
|
||||
if inspect(self).detached:
|
||||
db.add(self)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_update
|
||||
def delete(cls, db: Session, rid: Any) -> None:
|
||||
"""兼容旧插件调用:按主键删除当前模型并提交。"""
|
||||
"""在调用方同步事务中按主键删除当前模型。"""
|
||||
db.execute(delete(cls).where(and_(cls.id == rid)))
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_update
|
||||
async def async_delete(cls, db: AsyncSession, rid: Any) -> None:
|
||||
"""兼容旧插件调用:异步按主键删除当前模型并提交。"""
|
||||
"""在调用方异步事务中按主键删除当前模型。"""
|
||||
result = await db.execute(select(cls).where(and_(cls.id == rid)))
|
||||
user = result.scalars().first()
|
||||
if user:
|
||||
await db.delete(user)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_update
|
||||
def truncate(cls, db: Session) -> None:
|
||||
"""兼容旧插件调用:清空当前模型表并提交。"""
|
||||
"""在调用方同步事务中清空当前模型表。"""
|
||||
db.execute(delete(cls))
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_update
|
||||
async def async_truncate(cls, db: AsyncSession) -> None:
|
||||
"""兼容旧插件调用:异步清空当前模型表并提交。"""
|
||||
"""在调用方异步事务中清空当前模型表。"""
|
||||
await db.execute(delete(cls))
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list(cls, db: Session) -> List[Self]:
|
||||
"""兼容旧插件调用:查询当前模型的全部记录。"""
|
||||
"""在调用方同步会话中查询当前模型的全部记录。"""
|
||||
return list(db.execute(select(cls)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list(cls, db: AsyncSession) -> List[Self]:
|
||||
"""兼容旧插件调用:异步查询当前模型的全部记录。"""
|
||||
"""在调用方异步会话中查询当前模型的全部记录。"""
|
||||
result = await db.execute(select(cls))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -183,19 +166,19 @@ class DbOper:
|
||||
"""
|
||||
|
||||
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
|
||||
"""保存调用方会话;无会话写入由组合根兼容事务执行器承接。"""
|
||||
"""保存调用方会话;无会话调用由组合根事务执行器承接。"""
|
||||
self._db = db
|
||||
|
||||
def _execute_sync_write(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在当前同步会话暂存,或委托组合根创建兼容事务。"""
|
||||
"""在当前同步会话暂存,或委托组合根创建事务。"""
|
||||
if self._db is None or isinstance(self._db, AsyncSession):
|
||||
# 旧调用可能在同一 Oper 上混用同步/异步方法;跨会话类型时使用匹配的
|
||||
# 兼容事务,不能把 AsyncSession 交给同步 SQLAlchemy API。
|
||||
# 独立事务,不能把 AsyncSession 交给同步 SQLAlchemy API。
|
||||
return run_sync_transaction(operation)
|
||||
return operation(self._db)
|
||||
|
||||
def _execute_sync_query(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在当前同步会话查询,或委托组合根创建一次性兼容会话。"""
|
||||
"""在当前同步会话查询,或委托组合根创建一次性会话。"""
|
||||
if self._db is None or isinstance(self._db, AsyncSession):
|
||||
return run_sync_transaction(operation)
|
||||
return operation(self._db)
|
||||
@@ -204,10 +187,9 @@ class DbOper:
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在当前异步会话暂存,或委托组合根创建兼容事务。"""
|
||||
"""在当前异步会话暂存,或委托组合根创建事务。"""
|
||||
if self._db is None or isinstance(self._db, Session):
|
||||
# 与查询装饰器的历史行为一致:同步会话不会被错误传入异步模型写入,
|
||||
# 而是由组合根另开匹配的异步事务。
|
||||
# 同步会话不会被错误传入异步模型写入,而是由组合根另开匹配的异步事务。
|
||||
return await run_async_transaction(operation)
|
||||
return await operation(self._db)
|
||||
|
||||
@@ -215,13 +197,13 @@ class DbOper:
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在当前异步会话查询,或委托组合根创建一次性兼容会话。"""
|
||||
"""在当前异步会话查询,或委托组合根创建一次性会话。"""
|
||||
if self._db is None or isinstance(self._db, Session):
|
||||
return await run_async_transaction(operation)
|
||||
return await operation(self._db)
|
||||
|
||||
def _stage_create(self, model: TModel) -> TModel:
|
||||
"""在显式同步事务中暂存新模型,不触发 Base 的兼容提交装饰器。"""
|
||||
"""在调用方或组合根持有的同步事务中暂存新模型。"""
|
||||
def stage(session: Session) -> TModel:
|
||||
"""把模型加入当前同步会话。"""
|
||||
session.add(model)
|
||||
|
||||
+2
-151
@@ -5,8 +5,8 @@
|
||||
未显式传入会话时自动创建,并在结束时归还——异步路径经 async_session_scope 收口,
|
||||
连接池与配额都在那里生效。
|
||||
|
||||
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,正式装饰器
|
||||
和 legacy 兼容壳的处理一致。理由与代价都要写明,别当成漏写的 raise:
|
||||
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛。理由与代价
|
||||
都要写明,别当成漏写的 raise:
|
||||
|
||||
- 连接断开、事务已失效这类故障恰恰最容易发生在「出错之后」的收尾阶段。裸写收尾语句时
|
||||
它一抛错就顶替掉原始异常,调用方看到的只剩「connection reset」,业务异常连类型都被
|
||||
@@ -16,8 +16,6 @@
|
||||
SQLAlchemy 归还连接时已在池层吞掉异常并 invalidate 坏连接,再把释放故障升级成调用方
|
||||
的异常,只会让一次已经落库的写入看起来像失败,诱发重复提交。
|
||||
"""
|
||||
from functools import wraps
|
||||
from inspect import Parameter, signature
|
||||
from typing import Any, Awaitable, Callable, Optional, TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -279,150 +277,3 @@ def async_db_query(func: Callable[..., Awaitable[_R]]) -> Callable[..., Awaitabl
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def legacy_db_query(func: Callable[..., _R]) -> Callable[..., _R]:
|
||||
"""保留旧 Model 查询 ABI,同时让新调用方复用显式 Session。
|
||||
|
||||
旧插件通常省略 ``db``,直接把业务参数放在第一个位置;通用 ``db_query``
|
||||
装饰器只适用于固定的 ``(db, ...)`` 形状,不能把这类位置参数直接套进去。
|
||||
这里按签名插入会话,避免丢失旧插件传入的第一个业务参数。
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> _R:
|
||||
db = _get_args_db(args, kwargs)
|
||||
if db is not None:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
session = ScopedSession()
|
||||
call_args, call_kwargs = _inject_legacy_db(func, args, kwargs, session)
|
||||
try:
|
||||
return func(*call_args, **call_kwargs)
|
||||
finally:
|
||||
try:
|
||||
session.close()
|
||||
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 返回值
|
||||
logger.error(f"释放数据库会话失败:{close_err}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def legacy_async_db_query(
|
||||
func: Callable[..., Awaitable[_R]],
|
||||
) -> Callable[..., Awaitable[_R]]:
|
||||
"""保留旧 Model 异步查询 ABI,同时让新调用方复用显式 AsyncSession。"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> _R:
|
||||
db = _get_args_async_db(args, kwargs)
|
||||
if db is not None:
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
async with async_session_scope() as session:
|
||||
call_args, call_kwargs = _inject_legacy_db(func, args, kwargs, session)
|
||||
return await func(*call_args, **call_kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def legacy_db_update(func: Callable[..., _R]) -> Callable[..., _R]:
|
||||
"""保留旧 Model 同步写 ABI,并维持历史自动提交语义。
|
||||
|
||||
该装饰器只供已经公开的 Model/Base 方法兼容仓外插件。宿主新写路径必须
|
||||
通过 Application Command、显式 Session 和 UnitOfWork 完成事务收口。
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> _R:
|
||||
db = _get_args_db(args, kwargs)
|
||||
owns_session = db is None
|
||||
if db is None:
|
||||
db = ScopedSession()
|
||||
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
db.commit()
|
||||
return result
|
||||
except Exception:
|
||||
try:
|
||||
db.rollback()
|
||||
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
|
||||
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
|
||||
raise
|
||||
finally:
|
||||
if owns_session:
|
||||
try:
|
||||
db.close()
|
||||
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
|
||||
logger.error(f"释放数据库会话失败:{close_err}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def legacy_async_db_update(
|
||||
func: Callable[..., Awaitable[_R]],
|
||||
) -> Callable[..., Awaitable[_R]]:
|
||||
"""保留旧 Model 异步写 ABI,并维持历史自动提交语义。
|
||||
|
||||
该装饰器只承接既有兼容面;新宿主代码不得用它创建隐式事务。
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> _R:
|
||||
db = _get_args_async_db(args, kwargs)
|
||||
owns_session = db is None
|
||||
scope = None
|
||||
if db is None:
|
||||
scope = async_session_scope()
|
||||
db = await scope.__aenter__()
|
||||
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
await db.commit()
|
||||
return result
|
||||
except Exception:
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
|
||||
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
|
||||
raise
|
||||
finally:
|
||||
if owns_session and scope is not None:
|
||||
try:
|
||||
await scope.__aexit__(None, None, None)
|
||||
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
|
||||
logger.error(f"释放数据库会话失败:{close_err}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _inject_legacy_db(
|
||||
func: Callable[..., _R],
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
db: Any,
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
"""按旧 Model 方法签名注入兼容会话,不吞掉位置业务参数。"""
|
||||
call_args = list(args)
|
||||
call_kwargs = dict(kwargs)
|
||||
parameters = list(signature(func).parameters.values())
|
||||
db_index = next(
|
||||
(index for index, parameter in enumerate(parameters) if parameter.name == "db"),
|
||||
None,
|
||||
)
|
||||
if "db" in call_kwargs:
|
||||
call_kwargs["db"] = db
|
||||
return tuple(call_args), call_kwargs
|
||||
if db_index is None:
|
||||
# 兼容没有显式 db 参数的极旧函数,保持调用失败方式与普通 Python 一致。
|
||||
return tuple(call_args), {"db": db, **call_kwargs}
|
||||
if db_index < len(call_args) and call_args[db_index] is None:
|
||||
call_args[db_index] = db
|
||||
elif db_index < len(parameters) and parameters[db_index].kind is Parameter.POSITIONAL_ONLY:
|
||||
call_args.insert(db_index, db)
|
||||
elif db_index <= len(call_args):
|
||||
call_args.insert(db_index, db)
|
||||
else:
|
||||
call_kwargs["db"] = db
|
||||
return tuple(call_args), call_kwargs
|
||||
|
||||
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class AgentChat(Base):
|
||||
@@ -50,7 +49,6 @@ class AgentChat(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_session(
|
||||
cls, db: Session, session_id: str, user_id: Optional[str] = None
|
||||
) -> Optional["AgentChat"]:
|
||||
@@ -63,7 +61,6 @@ class AgentChat(Base):
|
||||
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_session(
|
||||
cls, db: AsyncSession, session_id: str, user_id: Optional[str] = None
|
||||
) -> Optional["AgentChat"]:
|
||||
@@ -77,7 +74,6 @@ class AgentChat(Base):
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_page(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -103,7 +99,6 @@ class AgentChat(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_page(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
|
||||
+11
-31
@@ -4,7 +4,6 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_db_query
|
||||
|
||||
|
||||
def _get_for_user_statement(
|
||||
@@ -85,47 +84,28 @@ class AgentTask(Base):
|
||||
return task.id
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_for_user(
|
||||
cls,
|
||||
db: Session | int | None = None,
|
||||
task_id: int | None = None,
|
||||
db: Session,
|
||||
task_id: int,
|
||||
user_id: Optional[str] = None,
|
||||
) -> Optional["AgentTask"]:
|
||||
"""
|
||||
按任务 ID 和可选用户 ID 查询,并保留无 Session 的旧插件调用方式。
|
||||
"""
|
||||
if task_id is None and isinstance(db, int):
|
||||
task_id, db = db, None
|
||||
if task_id is None:
|
||||
raise TypeError("task_id is required")
|
||||
|
||||
def query(session: Session) -> Optional["AgentTask"]:
|
||||
"""在给定会话中读取单个 Agent 任务。"""
|
||||
return session.execute(
|
||||
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
"""在调用方会话中按任务 ID 和可选用户 ID 查询。"""
|
||||
return db.execute(
|
||||
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_for_user(
|
||||
cls,
|
||||
db: Session | None = None,
|
||||
db: Session,
|
||||
user_id: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
) -> list["AgentTask"]:
|
||||
"""
|
||||
按用户和启用状态查询,并保留无 Session 的旧插件调用方式。
|
||||
"""
|
||||
def query(session: Session) -> list["AgentTask"]:
|
||||
"""在给定会话中读取 Agent 任务列表。"""
|
||||
return list(session.execute(
|
||||
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
|
||||
).scalars().all())
|
||||
|
||||
return query(db)
|
||||
"""在调用方会话中按用户和启用状态查询。"""
|
||||
return list(db.execute(
|
||||
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
def update_task(
|
||||
|
||||
@@ -4,7 +4,6 @@ from sqlalchemy import Index, Integer, String, Text, delete, select, update
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_db_query
|
||||
from app.db.models.agenttask import AgentTask
|
||||
|
||||
|
||||
@@ -249,7 +248,6 @@ class AgentTaskRun(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_run_id(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -261,7 +259,6 @@ class AgentTaskRun(Base):
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_for_task(
|
||||
cls,
|
||||
db: Session,
|
||||
|
||||
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
@@ -77,7 +76,6 @@ class DownloadHistory(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_hash(cls, db: Session, download_hash: str):
|
||||
return db.execute(
|
||||
select(DownloadHistory)
|
||||
@@ -86,7 +84,6 @@ class DownloadHistory(Base):
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_hashes(cls, db: Session, download_hashes: List[str]):
|
||||
"""
|
||||
批量查询多个下载任务的最新历史记录,避免在上层形成 N+1 查询。
|
||||
@@ -119,7 +116,6 @@ class DownloadHistory(Base):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_media_identity(
|
||||
cls, db: Session, media_source: MediaSource, media_id: str,
|
||||
music_type: Optional[str] = None,
|
||||
@@ -136,7 +132,6 @@ class DownloadHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_page(
|
||||
cls, db: Session, page: int = 1, count: int = 30
|
||||
):
|
||||
@@ -148,7 +143,6 @@ class DownloadHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_page(
|
||||
cls, db: AsyncSession, page: int = 1, count: int = 30
|
||||
):
|
||||
@@ -161,7 +155,6 @@ class DownloadHistory(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_title(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
@@ -177,13 +170,11 @@ class DownloadHistory(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_count(cls, db: AsyncSession):
|
||||
result = await db.execute(select(func.count(cls.id)))
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_count_by_title(cls, db: AsyncSession, title: str):
|
||||
result = await db.execute(
|
||||
select(func.count(cls.id)).filter(_title_like(cls.title, title))
|
||||
@@ -191,14 +182,12 @@ class DownloadHistory(Base):
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_path(cls, db: Session, path: str):
|
||||
return db.execute(
|
||||
select(DownloadHistory).where(DownloadHistory.path == path)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_last_by(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -237,7 +226,6 @@ class DownloadHistory(Base):
|
||||
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_user_date(cls, db: Session, date: str, username: Optional[str] = None):
|
||||
"""
|
||||
查询某用户某时间之前的下载历史。
|
||||
@@ -256,7 +244,6 @@ class DownloadHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_date(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -282,7 +269,6 @@ class DownloadHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_type(cls, db: Session, mtype: str, days: int):
|
||||
return list(db.execute(
|
||||
select(DownloadHistory).where(
|
||||
@@ -345,7 +331,6 @@ class DownloadFiles(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_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:
|
||||
@@ -353,7 +338,6 @@ class DownloadFiles(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False):
|
||||
result = db.execute(
|
||||
select(cls).where(cls.fullpath == fullpath).order_by(cls.id.desc())
|
||||
@@ -361,7 +345,6 @@ class DownloadFiles(Base):
|
||||
return list(result.all()) if all_files else result.first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_savepath(cls, db: Session, savepath: str):
|
||||
return list(db.execute(select(cls).where(cls.savepath == savepath)).scalars().all())
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MediaSource
|
||||
|
||||
@@ -53,12 +52,10 @@ class MediaServerItem(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_itemid(cls, db: Session, item_id: str):
|
||||
return db.execute(select(cls).where(cls.item_id == item_id)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_server_itemid(cls, db: Session, server: str, item_id: str):
|
||||
return db.execute(
|
||||
select(cls).where(cls.server == server, cls.item_id == item_id)
|
||||
@@ -97,7 +94,6 @@ class MediaServerItem(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exist_by_media_identity(
|
||||
cls, db: Session, media_source: MediaSource, media_id: str, mtype: str,
|
||||
):
|
||||
@@ -109,7 +105,6 @@ class MediaServerItem(Base):
|
||||
)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exists_by_title(cls, db: Session, title: str, mtype: str, year: str):
|
||||
statement = select(cls).where(cls.title == title)
|
||||
if mtype:
|
||||
@@ -119,13 +114,11 @@ class MediaServerItem(Base):
|
||||
return db.execute(statement).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_itemid(cls, db: AsyncSession, item_id: str):
|
||||
result = await db.execute(select(cls).filter(cls.item_id == item_id))
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_exist_by_media_identity(
|
||||
cls, db: AsyncSession, media_source: MediaSource, media_id: str, mtype: str,
|
||||
):
|
||||
@@ -138,7 +131,6 @@ class MediaServerItem(Base):
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_exists_by_title(cls, db: AsyncSession, title: str, mtype: str, year: str):
|
||||
if not mtype and not year:
|
||||
result = await db.execute(select(cls).filter(cls.title == title))
|
||||
|
||||
+45
-73
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class Message(Base):
|
||||
@@ -49,33 +48,25 @@ class Message(Base):
|
||||
return self.to_dict()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_page(
|
||||
cls,
|
||||
db: Session | None = None,
|
||||
db: Session,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
) -> List["Message"]:
|
||||
"""
|
||||
分页获取消息记录,兼容显式会话和旧插件无会话调用。
|
||||
"""
|
||||
def query(session: Session) -> List["Message"]:
|
||||
"""在给定同步会话中执行消息分页查询。"""
|
||||
return list(session.execute(
|
||||
select(cls)
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
).scalars().all())
|
||||
|
||||
return query(db)
|
||||
"""在调用方同步会话中分页获取消息记录。"""
|
||||
return list(db.execute(
|
||||
select(cls)
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exists_by_source(
|
||||
cls,
|
||||
db: Session | str | None = None,
|
||||
source: str | None = None,
|
||||
db: Session,
|
||||
source: str,
|
||||
) -> bool:
|
||||
"""
|
||||
判断指定来源标识的消息记录是否存在。
|
||||
@@ -84,44 +75,29 @@ class Message(Base):
|
||||
:param source: 消息来源唯一标识
|
||||
:return: 是否存在匹配记录
|
||||
"""
|
||||
if source is None and isinstance(db, str):
|
||||
source, db = db, None
|
||||
if source is None:
|
||||
raise TypeError("source is required")
|
||||
|
||||
def query(session: Session) -> bool:
|
||||
"""在给定同步会话中执行来源存在性查询。"""
|
||||
return session.execute(
|
||||
select(cls.id).where(cls.source == source).limit(1)
|
||||
).scalars().first() is not None
|
||||
|
||||
return query(db)
|
||||
return db.execute(
|
||||
select(cls.id).where(cls.source == source).limit(1)
|
||||
).scalars().first() is not None
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_page(
|
||||
cls, db: AsyncSession | None = None, page: int = 1, count: int = 30
|
||||
cls, db: AsyncSession, page: int = 1, count: int = 30
|
||||
) -> List["Message"]:
|
||||
"""
|
||||
异步分页获取消息记录。
|
||||
"""
|
||||
async def query(session: AsyncSession) -> List["Message"]:
|
||||
"""在给定异步会话中执行消息分页查询。"""
|
||||
result = await session.execute(
|
||||
select(cls)
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
return await query(db)
|
||||
result = await db.execute(
|
||||
select(cls)
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_sent_by_page(
|
||||
cls,
|
||||
db: AsyncSession | None = None,
|
||||
db: AsyncSession,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
all_clear_before: Optional[str] = None,
|
||||
@@ -131,35 +107,31 @@ class Message(Base):
|
||||
"""
|
||||
分页获取系统发送的通知消息。
|
||||
"""
|
||||
async def query(session: AsyncSession) -> List["Message"]:
|
||||
"""在给定异步会话中执行通知消息分页查询。"""
|
||||
statement = select(cls).where(cls.action == 1)
|
||||
if all_clear_before:
|
||||
statement = statement.where(cls.reg_time > all_clear_before)
|
||||
if system_clear_before:
|
||||
statement = statement.where(
|
||||
or_(
|
||||
and_(cls.image.isnot(None), cls.image != ""),
|
||||
cls.reg_time > system_clear_before,
|
||||
)
|
||||
statement = select(cls).where(cls.action == 1)
|
||||
if all_clear_before:
|
||||
statement = statement.where(cls.reg_time > all_clear_before)
|
||||
if system_clear_before:
|
||||
statement = statement.where(
|
||||
or_(
|
||||
and_(cls.image.isnot(None), cls.image != ""),
|
||||
cls.reg_time > system_clear_before,
|
||||
)
|
||||
if media_clear_before:
|
||||
statement = statement.where(
|
||||
or_(
|
||||
cls.image.is_(None),
|
||||
cls.image == "",
|
||||
cls.reg_time > media_clear_before,
|
||||
)
|
||||
)
|
||||
result = await session.execute(
|
||||
statement
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
return await query(db)
|
||||
if media_clear_before:
|
||||
statement = statement.where(
|
||||
or_(
|
||||
cls.image.is_(None),
|
||||
cls.image == "",
|
||||
cls.reg_time > media_clear_before,
|
||||
)
|
||||
)
|
||||
result = await db.execute(
|
||||
statement
|
||||
.order_by(cls.reg_time.desc(), cls.id.desc())
|
||||
.offset((page - 1) * count)
|
||||
.limit(count)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
def delete_before(
|
||||
|
||||
+16
-44
@@ -5,10 +5,6 @@ from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
from datetime import datetime
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import (
|
||||
legacy_async_db_query,
|
||||
legacy_db_query,
|
||||
)
|
||||
|
||||
|
||||
def _get_by_user_id_statement(model: type["PassKey"], user_id: int):
|
||||
@@ -54,75 +50,51 @@ class PassKey(Base):
|
||||
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_user_id(
|
||||
cls,
|
||||
db: Session | int | None = None,
|
||||
user_id: int | None = None,
|
||||
db: Session,
|
||||
user_id: int,
|
||||
):
|
||||
"""获取用户的所有 PassKey,并保留无 Session 的旧插件调用方式。"""
|
||||
if user_id is None and isinstance(db, int):
|
||||
user_id, db = db, None
|
||||
if user_id is None:
|
||||
raise TypeError("user_id is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行启用凭证查询。"""
|
||||
return list(session.execute(
|
||||
_get_by_user_id_statement(cls, user_id)
|
||||
).scalars().all())
|
||||
|
||||
return query(db)
|
||||
"""在调用方 Session 中获取用户的所有启用 PassKey。"""
|
||||
return list(db.execute(
|
||||
_get_by_user_id_statement(cls, user_id)
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_user_id(cls, db: AsyncSession, user_id: int):
|
||||
"""异步获取用户的所有 PassKey,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 AsyncSession 中获取用户的所有启用 PassKey。"""
|
||||
result = await db.execute(
|
||||
_get_by_user_id_statement(cls, user_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_credential_id(
|
||||
cls,
|
||||
db: Session | str | None = None,
|
||||
credential_id: str | None = None,
|
||||
db: Session,
|
||||
credential_id: str,
|
||||
):
|
||||
"""按凭证 ID 获取 PassKey,并保留无 Session 的旧插件调用方式。"""
|
||||
if credential_id is None and isinstance(db, str):
|
||||
credential_id, db = db, None
|
||||
if credential_id is None:
|
||||
raise TypeError("credential_id is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行启用凭证查询。"""
|
||||
return session.execute(
|
||||
_get_by_credential_id_statement(cls, credential_id)
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
"""在调用方 Session 中按凭证 ID 获取启用 PassKey。"""
|
||||
return db.execute(
|
||||
_get_by_credential_id_statement(cls, credential_id)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_credential_id(cls, db: AsyncSession, credential_id: str):
|
||||
"""异步根据凭证 ID 获取 PassKey,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 AsyncSession 中根据凭证 ID 获取启用 PassKey。"""
|
||||
result = await db.execute(
|
||||
_get_by_credential_id_statement(cls, credential_id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_id(cls, db: Session, passkey_id: int):
|
||||
"""根据 ID 获取 PassKey,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 Session 中根据 ID 获取 PassKey。"""
|
||||
return db.execute(select(cls).where(cls.id == passkey_id)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_id(cls, db: AsyncSession, passkey_id: int):
|
||||
"""异步根据 ID 获取 PassKey,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 AsyncSession 中根据 ID 获取 PassKey。"""
|
||||
result = await db.execute(
|
||||
select(cls).filter(cls.id == passkey_id)
|
||||
)
|
||||
|
||||
+12
-31
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class PluginData(Base):
|
||||
@@ -21,44 +20,32 @@ class PluginData(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_plugin_data(cls, db: Session | None = None, plugin_id: str | None = None):
|
||||
"""在调用方 Session 中读取插件全部数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None:
|
||||
raise TypeError("plugin_id is required")
|
||||
def get_plugin_data(cls, db: Session, plugin_id: str):
|
||||
"""在调用方 Session 中读取插件全部数据。"""
|
||||
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_plugin_data(
|
||||
cls, db: AsyncSession | None = None, plugin_id: str | None = None
|
||||
cls, db: AsyncSession, plugin_id: str
|
||||
):
|
||||
"""在调用方 AsyncSession 中读取插件全部数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None:
|
||||
raise TypeError("plugin_id is required")
|
||||
"""在调用方 AsyncSession 中读取插件全部数据。"""
|
||||
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_plugin_data_by_key(
|
||||
cls, db: Session | None = None, plugin_id: str | None = None, key: str | None = None
|
||||
cls, db: Session, plugin_id: str, key: str
|
||||
):
|
||||
"""在调用方 Session 中按键读取插件数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None or key is None:
|
||||
raise TypeError("plugin_id and key are required")
|
||||
"""在调用方 Session 中按键读取插件数据。"""
|
||||
return db.execute(
|
||||
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_plugin_data_by_key(
|
||||
cls, db: AsyncSession | None = None, plugin_id: str | None = None, key: str | None = None
|
||||
cls, db: AsyncSession, plugin_id: str, key: str
|
||||
):
|
||||
"""在调用方 AsyncSession 中按键读取插件数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None or key is None:
|
||||
raise TypeError("plugin_id and key are required")
|
||||
"""在调用方 AsyncSession 中按键读取插件数据。"""
|
||||
result = await db.execute(
|
||||
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
|
||||
)
|
||||
@@ -75,22 +62,16 @@ class PluginData(Base):
|
||||
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_plugin_data_by_plugin_id(
|
||||
cls, db: Session | None = None, plugin_id: str | None = None
|
||||
cls, db: Session, plugin_id: str
|
||||
):
|
||||
"""在调用方 Session 中按插件 ID 读取数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None:
|
||||
raise TypeError("plugin_id is required")
|
||||
"""在调用方 Session 中按插件 ID 读取数据。"""
|
||||
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_plugin_data_by_plugin_id(
|
||||
cls, db: AsyncSession | None = None, plugin_id: str | None = None
|
||||
cls, db: AsyncSession, plugin_id: str
|
||||
):
|
||||
"""在调用方 AsyncSession 中按插件 ID 读取数据,并兼容旧无会话入口。"""
|
||||
if plugin_id is None:
|
||||
raise TypeError("plugin_id is required")
|
||||
"""在调用方 AsyncSession 中按插件 ID 读取数据。"""
|
||||
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
|
||||
return list(result.scalars().all())
|
||||
|
||||
+35
-92
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class Site(Base):
|
||||
@@ -58,122 +57,66 @@ class Site(Base):
|
||||
downloader: Mapped[Optional[str]] = mapped_column(String)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_domain(cls, db: Session | str | None = None, domain: str | None = None):
|
||||
"""按域名查询站点,兼容显式会话和旧插件无会话调用。"""
|
||||
if domain is None and isinstance(db, str):
|
||||
domain, db = db, None
|
||||
if domain is None:
|
||||
raise TypeError("domain is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定同步会话中执行域名查询。"""
|
||||
return session.execute(select(cls).where(cls.domain == domain)).scalars().first()
|
||||
|
||||
return query(db)
|
||||
def get_by_domain(cls, db: Session, domain: str):
|
||||
"""在调用方 Session 中按域名查询站点。"""
|
||||
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_domain(
|
||||
cls,
|
||||
db: AsyncSession | str | None = None,
|
||||
domain: str | None = None,
|
||||
db: AsyncSession,
|
||||
domain: str,
|
||||
):
|
||||
"""异步按域名查询站点,兼容显式会话和旧插件无会话调用。"""
|
||||
if domain is None and isinstance(db, str):
|
||||
domain, db = db, None
|
||||
if domain is None:
|
||||
raise TypeError("domain is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行域名查询。"""
|
||||
result = await session.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按域名查询站点。"""
|
||||
result = await db.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_name(
|
||||
cls,
|
||||
db: AsyncSession | str | None = None,
|
||||
name: str | None = None,
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
):
|
||||
"""异步按站点名称查询,兼容显式会话和旧插件无会话调用。"""
|
||||
if name is None and isinstance(db, str):
|
||||
name, db = db, None
|
||||
if name is None:
|
||||
raise TypeError("name is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行名称查询。"""
|
||||
result = await session.execute(select(cls).where(cls.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按站点名称查询。"""
|
||||
result = await db.execute(select(cls).where(cls.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_actives(cls, db: Session | None = None):
|
||||
"""查询启用站点,兼容显式会话和旧插件无会话调用。"""
|
||||
def query(session: Session):
|
||||
"""在给定同步会话中执行启用站点查询。"""
|
||||
return list(session.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
|
||||
|
||||
return query(db)
|
||||
def get_actives(cls, db: Session):
|
||||
"""在调用方 Session 中查询启用站点。"""
|
||||
return list(db.execute(
|
||||
select(cls).where(cls.is_active.is_(True))
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_actives(cls, db: AsyncSession | None = None):
|
||||
"""异步查询启用站点,兼容显式会话和旧插件无会话调用。"""
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行启用站点查询。"""
|
||||
result = await session.execute(select(cls).where(cls.is_active.is_(True)))
|
||||
return list(result.scalars().all())
|
||||
|
||||
return await query(db)
|
||||
async def async_get_actives(cls, db: AsyncSession):
|
||||
"""在调用方 AsyncSession 中查询启用站点。"""
|
||||
result = await db.execute(select(cls).where(cls.is_active.is_(True)))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_order_by_pri(cls, db: Session | None = None):
|
||||
"""按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
|
||||
def query(session: Session):
|
||||
"""在给定同步会话中执行优先级查询。"""
|
||||
return list(session.execute(select(cls).order_by(cls.pri)).scalars().all())
|
||||
|
||||
return query(db)
|
||||
def list_order_by_pri(cls, db: Session):
|
||||
"""在调用方 Session 中按优先级升序查询站点。"""
|
||||
return list(db.execute(select(cls).order_by(cls.pri)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_order_by_pri(cls, db: AsyncSession | None = None):
|
||||
"""异步按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行优先级查询。"""
|
||||
result = await session.execute(select(cls).order_by(cls.pri))
|
||||
return list(result.scalars().all())
|
||||
|
||||
return await query(db)
|
||||
async def async_list_order_by_pri(cls, db: AsyncSession):
|
||||
"""在调用方 AsyncSession 中按优先级升序查询站点。"""
|
||||
result = await db.execute(select(cls).order_by(cls.pri))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_domains_by_ids(
|
||||
cls,
|
||||
db: Session | list[int] | None = None,
|
||||
ids: list[int] | None = None,
|
||||
db: Session,
|
||||
ids: list[int],
|
||||
):
|
||||
"""按 ID 查询域名,兼容显式会话和旧插件无会话调用。"""
|
||||
if ids is None and isinstance(db, list):
|
||||
ids, db = db, None
|
||||
if ids is None:
|
||||
raise TypeError("ids is required")
|
||||
"""在调用方 Session 中按 ID 查询域名。"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定同步会话中执行域名投影查询。"""
|
||||
return list(session.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
|
||||
|
||||
return query(db)
|
||||
return list(db.execute(
|
||||
select(cls.domain).where(cls.id.in_(ids))
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
def reset(cls, db: Session):
|
||||
|
||||
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query
|
||||
|
||||
|
||||
class SiteIcon(Base):
|
||||
@@ -27,19 +26,11 @@ class SiteIcon(Base):
|
||||
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_domain(
|
||||
cls,
|
||||
db: AsyncSession | None = None,
|
||||
domain: str | None = None,
|
||||
db: AsyncSession,
|
||||
domain: str,
|
||||
):
|
||||
"""在调用方 AsyncSession 中查询站点图标。"""
|
||||
if domain is None:
|
||||
raise TypeError("domain is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行站点图标查询。"""
|
||||
result = await session.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
return await query(db)
|
||||
result = await db.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import legacy_async_db_query
|
||||
|
||||
|
||||
class SiteStatistic(Base):
|
||||
@@ -35,22 +34,14 @@ class SiteStatistic(Base):
|
||||
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_domain(
|
||||
cls,
|
||||
db: AsyncSession | None = None,
|
||||
domain: str | None = None,
|
||||
db: AsyncSession,
|
||||
domain: str,
|
||||
):
|
||||
"""在调用方 AsyncSession 中查询站点统计,并兼容旧无会话调用。"""
|
||||
if domain is None:
|
||||
raise TypeError("domain is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行站点统计查询。"""
|
||||
result = await session.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中查询站点统计。"""
|
||||
result = await db.execute(select(cls).where(cls.domain == domain))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
def reset(cls, db: Session):
|
||||
|
||||
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class SiteUserData(Base):
|
||||
@@ -61,7 +60,6 @@ class SiteUserData(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_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:
|
||||
@@ -72,7 +70,6 @@ class SiteUserData(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_domain(cls, db: AsyncSession, domain: str, workdate: Optional[str] = None, worktime: Optional[str] = None):
|
||||
query = select(cls).filter(cls.domain == domain)
|
||||
if workdate and worktime:
|
||||
@@ -83,12 +80,10 @@ class SiteUserData(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_date(cls, db: Session, date: str):
|
||||
return list(db.execute(select(cls).where(cls.updated_day == date)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_latest(cls, db: Session):
|
||||
"""
|
||||
获取各站点最新一天的数据
|
||||
@@ -113,7 +108,6 @@ class SiteUserData(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_latest(cls, db: AsyncSession):
|
||||
"""
|
||||
异步获取各站点最新一天的数据
|
||||
|
||||
+107
-210
@@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import get_id_column, Base
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
|
||||
|
||||
@@ -140,9 +139,8 @@ class Subscribe(Base):
|
||||
return condition
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exists(
|
||||
cls, db: Session | MediaSource | None = None,
|
||||
cls, db: Session,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
season: Optional[int] = None,
|
||||
@@ -150,27 +148,21 @@ class Subscribe(Base):
|
||||
music_type: Optional[str] = None,
|
||||
):
|
||||
"""按媒体身份、季号与剧集组查询已有订阅。"""
|
||||
if db is not None and not isinstance(db, Session):
|
||||
media_source, media_id, db = db, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source, media_id, music_type
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行订阅身份查询。"""
|
||||
statement = select(cls).where(condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return session.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
).scalars().first()
|
||||
return query(db)
|
||||
statement = select(cls).where(condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return db.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_exists(
|
||||
cls, db: AsyncSession | MediaSource | None = None,
|
||||
cls, db: AsyncSession,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
season: Optional[int] = None,
|
||||
@@ -178,28 +170,22 @@ class Subscribe(Base):
|
||||
music_type: Optional[str] = None,
|
||||
):
|
||||
"""异步按媒体身份、季号与剧集组查询已有订阅。"""
|
||||
if db is not None and not isinstance(db, AsyncSession):
|
||||
media_source, media_id, db = db, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source, media_id, music_type
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行订阅身份查询。"""
|
||||
statement = select(cls).where(condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await session.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
)
|
||||
return result.scalars().first()
|
||||
return await query(db)
|
||||
statement = select(cls).where(condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await db.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exists_by_username(
|
||||
cls, db: Session | str | None = None,
|
||||
cls, db: Session,
|
||||
username: str | MediaSource | None = None,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
@@ -210,8 +196,6 @@ class Subscribe(Base):
|
||||
"""
|
||||
按订阅 owner、媒体身份、季号与剧集组查询订阅行。
|
||||
"""
|
||||
if db is not None and not isinstance(db, Session):
|
||||
username, media_source, media_id, db = db, username, media_source, None
|
||||
if not username:
|
||||
return None
|
||||
condition = cls._identity_condition(
|
||||
@@ -219,20 +203,16 @@ class Subscribe(Base):
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行订阅 owner 查询。"""
|
||||
statement = select(cls).where(cls.username == username, condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return session.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
).scalars().first()
|
||||
return query(db)
|
||||
statement = select(cls).where(cls.username == username, condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return db.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_exists_by_username(
|
||||
cls, db: AsyncSession | str | None = None,
|
||||
cls, db: AsyncSession,
|
||||
username: str | MediaSource | None = None,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None, season: Optional[int] = None,
|
||||
@@ -242,8 +222,6 @@ class Subscribe(Base):
|
||||
"""
|
||||
异步按订阅 owner、媒体身份、季号与剧集组查询订阅行。
|
||||
"""
|
||||
if db is not None and not isinstance(db, AsyncSession):
|
||||
username, media_source, media_id, db = db, username, media_source, None
|
||||
if not username:
|
||||
return None
|
||||
condition = cls._identity_condition(
|
||||
@@ -251,112 +229,76 @@ class Subscribe(Base):
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行订阅 owner 查询。"""
|
||||
statement = select(cls).where(cls.username == username, condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await session.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
)
|
||||
return result.scalars().first()
|
||||
return await query(db)
|
||||
statement = select(cls).where(cls.username == username, condition)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await db.execute(
|
||||
statement.where(cls.episode_group == episode_group)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_state(cls, db: Session | str | None = None, state: str | None = None):
|
||||
"""按状态列表查询订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, Session):
|
||||
state, db = db if state is None else state, None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行状态查询。"""
|
||||
statement = select(cls)
|
||||
if state:
|
||||
statement = statement.where(cls.state.in_(state.split(',')))
|
||||
return list(session.execute(statement).scalars().all())
|
||||
return query(db)
|
||||
def get_by_state(cls, db: Session, state: str | None = None):
|
||||
"""在调用方 Session 中按状态列表查询订阅。"""
|
||||
statement = select(cls)
|
||||
if state:
|
||||
statement = statement.where(cls.state.in_(state.split(',')))
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_state(
|
||||
cls, db: AsyncSession | str | None = None, state: str | None = None
|
||||
cls, db: AsyncSession, state: str | None = None
|
||||
):
|
||||
"""异步按状态列表查询订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, AsyncSession):
|
||||
state, db = db if state is None else state, None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行状态查询。"""
|
||||
statement = select(cls)
|
||||
if state:
|
||||
statement = statement.where(cls.state.in_(state.split(',')))
|
||||
result = await session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按状态列表查询订阅。"""
|
||||
statement = select(cls)
|
||||
if state:
|
||||
statement = statement.where(cls.state.in_(state.split(',')))
|
||||
result = await db.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_title(
|
||||
cls, db: Session | str | None = None, title: str | None = None,
|
||||
cls, db: Session, title: str,
|
||||
season: Optional[int] = None,
|
||||
):
|
||||
"""按标题查询订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, Session):
|
||||
title, db = db if title is None else title, None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行标题查询。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return session.execute(statement).scalars().first()
|
||||
return query(db)
|
||||
"""在调用方 Session 中按标题查询订阅。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
return db.execute(statement).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_title(
|
||||
cls, db: AsyncSession | str | None = None, title: str | None = None,
|
||||
cls, db: AsyncSession, title: str,
|
||||
season: Optional[int] = None,
|
||||
):
|
||||
"""异步按标题查询订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, AsyncSession):
|
||||
title, db = db if title is None else title, None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行标题查询。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await session.execute(statement)
|
||||
return result.scalars().first()
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按标题查询订阅。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await db.execute(statement)
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_title(
|
||||
cls, db: AsyncSession | str | None = None, title: str | None = None,
|
||||
cls, db: AsyncSession, title: str,
|
||||
season: Optional[int] = None,
|
||||
):
|
||||
"""异步按标题查询候选订阅列表,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, AsyncSession):
|
||||
title, db = db if title is None else title, None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行标题列表查询。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按标题查询候选订阅列表。"""
|
||||
statement = select(cls).where(cls.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
result = await db.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_media_identity(
|
||||
cls, db: Session | MediaSource | None = None,
|
||||
cls, db: Session,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
music_type: Optional[str] = None,
|
||||
):
|
||||
"""同步按统一媒体身份查询候选订阅列表。"""
|
||||
if db is not None and not isinstance(db, Session):
|
||||
media_source, media_id, db = db, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
@@ -364,22 +306,16 @@ class Subscribe(Base):
|
||||
)
|
||||
if condition is None:
|
||||
return []
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行媒体身份列表查询。"""
|
||||
return list(session.execute(select(cls).where(condition)).scalars().all())
|
||||
return query(db)
|
||||
return list(db.execute(select(cls).where(condition)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_media_identity(
|
||||
cls, db: AsyncSession | MediaSource | None = None,
|
||||
cls, db: AsyncSession,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
music_type: Optional[str] = None,
|
||||
):
|
||||
"""异步按统一媒体身份查询候选订阅列表。"""
|
||||
if db is not None and not isinstance(db, AsyncSession):
|
||||
media_source, media_id, db = db, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
@@ -387,16 +323,12 @@ class Subscribe(Base):
|
||||
)
|
||||
if condition is None:
|
||||
return []
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行媒体身份列表查询。"""
|
||||
result = await session.execute(select(cls).where(condition))
|
||||
return list(result.scalars().all())
|
||||
return await query(db)
|
||||
result = await db.execute(select(cls).where(condition))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by(
|
||||
cls, db: Session | str | None = None,
|
||||
cls, db: Session,
|
||||
type: str | MediaSource | None = None,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
@@ -406,8 +338,6 @@ class Subscribe(Base):
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
if db is not None and not isinstance(db, Session):
|
||||
type, media_source, media_id, db = db, type, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source, media_id, music_type
|
||||
)
|
||||
@@ -416,15 +346,11 @@ class Subscribe(Base):
|
||||
statement = select(cls).where(condition, cls.type == type)
|
||||
if season is not None:
|
||||
statement = statement.where(cls.season == season)
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行类型媒体查询。"""
|
||||
return session.execute(statement).scalars().first()
|
||||
return query(db)
|
||||
return db.execute(statement).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by(
|
||||
cls, db: AsyncSession | str | None = None,
|
||||
cls, db: AsyncSession,
|
||||
type: str | MediaSource | None = None,
|
||||
media_source: MediaSource | str | None = None,
|
||||
media_id: str | None = None,
|
||||
@@ -434,8 +360,6 @@ class Subscribe(Base):
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
if db is not None and not isinstance(db, AsyncSession):
|
||||
type, media_source, media_id, db = db, type, media_source, None
|
||||
condition = cls._identity_condition(
|
||||
media_source, media_id, music_type
|
||||
)
|
||||
@@ -444,76 +368,49 @@ class Subscribe(Base):
|
||||
query = select(cls).filter(condition, cls.type == type)
|
||||
if season is not None:
|
||||
query = query.filter(cls.season == season)
|
||||
async def execute_query(session: AsyncSession):
|
||||
"""在给定异步会话中执行类型媒体查询。"""
|
||||
result = await session.execute(query)
|
||||
return result.scalars().first()
|
||||
return await execute_query(db)
|
||||
result = await db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_username(cls, db: Session | str | None = None, username: str | None = None,
|
||||
def list_by_username(cls, db: Session, username: str,
|
||||
state: Optional[str] = None, mtype: Optional[str] = None):
|
||||
"""按用户筛选订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, Session):
|
||||
username, db = db if username is None else username, None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行用户筛选查询。"""
|
||||
statement = select(cls).where(cls.username == username)
|
||||
if state:
|
||||
statement = statement.where(cls.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(cls.type == mtype)
|
||||
return list(session.execute(statement).scalars().all())
|
||||
return query(db)
|
||||
"""在调用方 Session 中按用户筛选订阅。"""
|
||||
statement = select(cls).where(cls.username == username)
|
||||
if state:
|
||||
statement = statement.where(cls.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(cls.type == mtype)
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_username(cls, db: AsyncSession | str | None = None,
|
||||
username: str | None = None, state: Optional[str] = None,
|
||||
async def async_list_by_username(cls, db: AsyncSession,
|
||||
username: str, state: Optional[str] = None,
|
||||
mtype: Optional[str] = None):
|
||||
"""异步按用户筛选订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, AsyncSession):
|
||||
username, db = db if username is None else username, None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行用户筛选查询。"""
|
||||
statement = select(cls).where(cls.username == username)
|
||||
if state:
|
||||
statement = statement.where(cls.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(cls.type == mtype)
|
||||
result = await session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
return await query(db)
|
||||
"""在调用方 AsyncSession 中按用户筛选订阅。"""
|
||||
statement = select(cls).where(cls.username == username)
|
||||
if state:
|
||||
statement = statement.where(cls.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(cls.type == mtype)
|
||||
result = await db.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_type(cls, db: Session | str | None = None, mtype: str | None = None, days: int = 7):
|
||||
"""按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, Session):
|
||||
mtype, db = db if mtype is None else mtype, None
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行时间窗订阅查询。"""
|
||||
return list(session.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())
|
||||
return query(db)
|
||||
def list_by_type(cls, db: Session, mtype: str, days: int = 7):
|
||||
"""在调用方 Session 中按类型查询最近时间窗内的订阅。"""
|
||||
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
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_type(cls, db: AsyncSession | str | None = None,
|
||||
mtype: str | None = None, days: int = 7):
|
||||
"""异步按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
|
||||
if not isinstance(db, AsyncSession):
|
||||
mtype, db = db if mtype is None else mtype, None
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行时间窗订阅查询。"""
|
||||
result = await session.execute(select(cls).where(
|
||||
cls.type == mtype,
|
||||
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(time.time() - 86400 * int(days)))
|
||||
))
|
||||
return list(result.scalars().all())
|
||||
return await query(db)
|
||||
async def async_list_by_type(cls, db: AsyncSession,
|
||||
mtype: str, days: int = 7):
|
||||
"""在调用方 AsyncSession 中按类型查询最近时间窗内的订阅。"""
|
||||
result = await 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)))
|
||||
))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
|
||||
|
||||
@@ -107,9 +106,8 @@ class SubscribeHistory(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_type(cls, db: Session, mtype: str, page: int = 1, count: int = 30):
|
||||
"""按媒体类型分页查询订阅历史,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 Session 中按媒体类型分页查询订阅历史。"""
|
||||
return list(db.execute(
|
||||
select(cls).where(
|
||||
cls.type == mtype
|
||||
@@ -119,9 +117,8 @@ class SubscribeHistory(Base):
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_type(cls, db: AsyncSession, mtype: str, page: int = 1, count: int = 30):
|
||||
"""异步按媒体类型分页查询订阅历史,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 AsyncSession 中按媒体类型分页查询订阅历史。"""
|
||||
result = await db.execute(
|
||||
select(cls).filter(
|
||||
cls.type == mtype
|
||||
@@ -132,7 +129,6 @@ class SubscribeHistory(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_type_and_username(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
@@ -177,7 +173,6 @@ class SubscribeHistory(Base):
|
||||
return condition
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def exists(
|
||||
cls, db: Session, media_source: MediaSource, media_id: str,
|
||||
season: Optional[int] = None,
|
||||
@@ -197,7 +192,6 @@ class SubscribeHistory(Base):
|
||||
return db.execute(statement).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_exists(
|
||||
cls, db: AsyncSession, media_source: MediaSource, media_id: str,
|
||||
season: Optional[int] = None,
|
||||
|
||||
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
@@ -18,15 +17,13 @@ class SystemConfig(Base):
|
||||
value: Mapped[Optional[Any]] = mapped_column(JSON)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_key(cls, db: Session, key: str):
|
||||
"""按配置键查询系统配置,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 Session 中按配置键查询系统配置。"""
|
||||
return db.execute(select(cls).where(cls.key == key)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_key(cls, db: AsyncSession, key: str):
|
||||
"""异步按配置键查询系统配置,并保留旧插件无 Session 调用。"""
|
||||
"""在调用方 AsyncSession 中按配置键查询系统配置。"""
|
||||
result = await db.execute(select(cls).where(cls.key == key))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import (
|
||||
legacy_async_db_query,
|
||||
legacy_db_query,
|
||||
)
|
||||
from app.db.models._constraints import media_identity_constraint
|
||||
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
|
||||
|
||||
@@ -97,7 +93,6 @@ class TransferHistory(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_title(cls, db: Session, title: str, page: int = 1, count: int = 30,
|
||||
status: Optional[bool] = None, wildcard: bool = False):
|
||||
if wildcard:
|
||||
@@ -124,7 +119,6 @@ class TransferHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
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:
|
||||
@@ -152,7 +146,6 @@ class TransferHistory(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
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:
|
||||
@@ -166,7 +159,6 @@ class TransferHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_list_by_page(cls, db: AsyncSession, page: int = 1, count: int = 30,
|
||||
status: Optional[bool] = None):
|
||||
if status is not None:
|
||||
@@ -188,30 +180,19 @@ class TransferHistory(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_hash(
|
||||
cls,
|
||||
db: Session | str | None = None,
|
||||
download_hash: str | None = None,
|
||||
db: Session,
|
||||
download_hash: str,
|
||||
):
|
||||
"""按下载哈希查询最新记录,兼容旧插件无会话调用。"""
|
||||
if download_hash is None and isinstance(db, str):
|
||||
download_hash, db = db, None
|
||||
if download_hash is None:
|
||||
raise TypeError("download_hash is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在调用方提供的同步会话中执行哈希查询。"""
|
||||
return session.execute(
|
||||
select(cls).where(cls.download_hash == download_hash)
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
"""在调用方 Session 中按下载哈希查询最新记录。"""
|
||||
return db.execute(
|
||||
select(cls).where(cls.download_hash == download_hash)
|
||||
).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_src(
|
||||
cls, db: Session | str | None = None, src: str | None = None,
|
||||
cls, db: Session, src: str,
|
||||
storage: Optional[str] = None
|
||||
) -> Optional["TransferHistory"]:
|
||||
"""
|
||||
@@ -222,26 +203,14 @@ class TransferHistory(Base):
|
||||
:param storage: 源存储类型
|
||||
:return: 命中的整理记录,未命中时返回 None
|
||||
"""
|
||||
if src is None and isinstance(db, str):
|
||||
src, db = db, None
|
||||
if src is None:
|
||||
raise TypeError("src is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在调用方提供的同步会话中执行源路径查询。"""
|
||||
statement = select(cls).where(cls.src == src)
|
||||
if storage:
|
||||
statement = statement.where(cls.src_storage == storage)
|
||||
return session.execute(
|
||||
statement.order_by(cls.id.desc())
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
statement = select(cls).where(cls.src == src)
|
||||
if storage:
|
||||
statement = statement.where(cls.src_storage == storage)
|
||||
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_success_by_src(
|
||||
cls, db: Session | str | None = None, src: str | None = None,
|
||||
cls, db: Session, src: str,
|
||||
storage: Optional[str] = None
|
||||
) -> Optional["TransferHistory"]:
|
||||
"""
|
||||
@@ -254,26 +223,14 @@ class TransferHistory(Base):
|
||||
:param storage: 源存储类型
|
||||
:return: 命中的成功整理记录,未命中时返回 None
|
||||
"""
|
||||
if src is None and isinstance(db, str):
|
||||
src, db = db, None
|
||||
if src is None:
|
||||
raise TypeError("src is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在调用方提供的同步会话中执行成功源路径查询。"""
|
||||
statement = select(cls).where(cls.src == src, cls.status.is_(True))
|
||||
if storage:
|
||||
statement = statement.where(cls.src_storage == storage)
|
||||
return session.execute(
|
||||
statement.order_by(cls.id.desc())
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
statement = select(cls).where(cls.src == src, cls.status.is_(True))
|
||||
if storage:
|
||||
statement = statement.where(cls.src_storage == storage)
|
||||
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_dest(
|
||||
cls, db: Session | str | None = None, dest: str | None = None,
|
||||
cls, db: Session, dest: str,
|
||||
storage: Optional[str] = None
|
||||
) -> Optional["TransferHistory"]:
|
||||
"""
|
||||
@@ -284,24 +241,12 @@ class TransferHistory(Base):
|
||||
:param storage: 目标存储类型
|
||||
:return: 命中的整理记录,未命中时返回 None
|
||||
"""
|
||||
if dest is None and isinstance(db, str):
|
||||
dest, db = db, None
|
||||
if dest is None:
|
||||
raise TypeError("dest is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在调用方提供的同步会话中执行目标路径查询。"""
|
||||
statement = select(cls).where(cls.dest == dest)
|
||||
if storage:
|
||||
statement = statement.where(cls.dest_storage == storage)
|
||||
return session.execute(
|
||||
statement.order_by(cls.id.desc())
|
||||
).scalars().first()
|
||||
|
||||
return query(db)
|
||||
statement = select(cls).where(cls.dest == dest)
|
||||
if storage:
|
||||
statement = statement.where(cls.dest_storage == storage)
|
||||
return db.execute(statement.order_by(cls.id.desc())).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_success_by_src(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -341,7 +286,6 @@ class TransferHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_success_move_by_dest(
|
||||
cls,
|
||||
db: Session,
|
||||
@@ -384,14 +328,12 @@ class TransferHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_hash(cls, db: Session, download_hash: str):
|
||||
return list(db.execute(
|
||||
select(cls).where(cls.download_hash == download_hash)
|
||||
).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def statistic(cls, db: Session, days: int = 7):
|
||||
"""
|
||||
统计最近days天的下载历史数量,按日期分组返回每日数量
|
||||
@@ -408,7 +350,6 @@ class TransferHistory(Base):
|
||||
).all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def monthly_media_statistics(cls, db: Session):
|
||||
"""
|
||||
统计当月成功整理的电影、电视剧、剧集和音乐数量。
|
||||
@@ -474,7 +415,6 @@ class TransferHistory(Base):
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_statistic(cls, db: AsyncSession, days: int = 7):
|
||||
"""
|
||||
统计最近days天的下载历史数量,按日期分组返回每日数量
|
||||
@@ -489,7 +429,6 @@ class TransferHistory(Base):
|
||||
return result.all()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def count(cls, db: Session, status: Optional[bool] = None):
|
||||
statement = select(func.count(cls.id))
|
||||
if status is not None:
|
||||
@@ -497,7 +436,6 @@ class TransferHistory(Base):
|
||||
return db.execute(statement).scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_count(cls, db: AsyncSession, status: Optional[bool] = None):
|
||||
if status is not None:
|
||||
result = await db.execute(
|
||||
@@ -510,7 +448,6 @@ class TransferHistory(Base):
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def count_by_title(cls, db: Session, title: str, status: Optional[bool] = None, wildcard: bool = False):
|
||||
if wildcard:
|
||||
text_filter = or_(
|
||||
@@ -530,7 +467,6 @@ class TransferHistory(Base):
|
||||
return db.execute(statement).scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_count_by_title(cls, db: AsyncSession, title: str, status: Optional[bool] = None, wildcard: bool = False):
|
||||
if wildcard:
|
||||
text_filter = or_(
|
||||
@@ -551,7 +487,6 @@ class TransferHistory(Base):
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by(cls, db: Session, mtype: Optional[str] = None, title: Optional[str] = None, year: Optional[str] = None,
|
||||
season: Optional[str] = None,
|
||||
episode: Optional[str] = None,
|
||||
@@ -589,7 +524,6 @@ class TransferHistory(Base):
|
||||
return list(db.execute(statement).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_media_identity(
|
||||
cls, db: Session, media_source: MediaSource, media_id: str,
|
||||
mtype: Optional[str] = None,
|
||||
@@ -636,7 +570,6 @@ class TransferHistory(Base):
|
||||
return history
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_by_date(cls, db: Session, date: str):
|
||||
"""
|
||||
查询某时间之后的转移历史
|
||||
|
||||
@@ -4,7 +4,6 @@ from sqlalchemy import Index, String, delete, select
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, execute_dml, get_id_column
|
||||
from app.db.decorators import legacy_db_query
|
||||
|
||||
|
||||
class TransferPending(Base):
|
||||
@@ -73,7 +72,6 @@ class TransferPending(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def list_all(cls, db: Session, limit: Optional[int] = 5000) -> List["TransferPending"]:
|
||||
"""
|
||||
列出全部待整理登记,供启动回放使用。
|
||||
|
||||
+17
-58
@@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class User(Base):
|
||||
@@ -35,78 +34,38 @@ class User(Base):
|
||||
settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_name(
|
||||
cls,
|
||||
db: Session | str | None = None,
|
||||
name: str | None = None,
|
||||
db: Session,
|
||||
name: str,
|
||||
):
|
||||
"""按用户名查询用户,兼容显式会话和旧插件无会话调用。"""
|
||||
if name is None and isinstance(db, str):
|
||||
name, db = db, None
|
||||
if name is None:
|
||||
raise TypeError("name is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行用户名查询。"""
|
||||
return session.execute(select(cls).where(cls.name == name)).scalars().first()
|
||||
|
||||
return query(db)
|
||||
"""在调用方同步会话中按用户名查询用户。"""
|
||||
return db.execute(select(cls).where(cls.name == name)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_name(
|
||||
cls,
|
||||
db: AsyncSession | str | None = None,
|
||||
name: str | None = None,
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
):
|
||||
"""异步按用户名查询,兼容显式会话和旧插件无会话调用。"""
|
||||
if name is None and isinstance(db, str):
|
||||
name, db = db, None
|
||||
if name is None:
|
||||
raise TypeError("name is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行用户名查询。"""
|
||||
result = await session.execute(select(cls).filter(cls.name == name))
|
||||
return result.scalars().first()
|
||||
|
||||
return await query(db)
|
||||
"""在调用方异步会话中按用户名查询用户。"""
|
||||
result = await db.execute(select(cls).filter(cls.name == name))
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None):
|
||||
"""按用户 ID 查询用户,兼容显式会话和旧插件无会话调用。"""
|
||||
if user_id is None and isinstance(db, int):
|
||||
user_id, db = db, None
|
||||
if user_id is None:
|
||||
raise TypeError("user_id is required")
|
||||
|
||||
def query(session: Session):
|
||||
"""在给定会话中执行用户 ID 查询。"""
|
||||
return session.execute(select(cls).where(cls.id == user_id)).scalars().first()
|
||||
|
||||
return query(db)
|
||||
def get_by_id(cls, db: Session, user_id: int):
|
||||
"""在调用方同步会话中按用户 ID 查询用户。"""
|
||||
return db.execute(select(cls).where(cls.id == user_id)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_id(
|
||||
cls,
|
||||
db: AsyncSession | int | None = None,
|
||||
user_id: int | None = None,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
):
|
||||
"""异步按用户 ID 查询,兼容显式会话和旧插件无会话调用。"""
|
||||
if user_id is None and isinstance(db, int):
|
||||
user_id, db = db, None
|
||||
if user_id is None:
|
||||
raise TypeError("user_id is required")
|
||||
|
||||
async def query(session: AsyncSession):
|
||||
"""在给定异步会话中执行用户 ID 查询。"""
|
||||
result = await session.execute(select(cls).filter(cls.id == user_id))
|
||||
return result.scalars().first()
|
||||
|
||||
return await query(db)
|
||||
"""在调用方异步会话中按用户 ID 查询用户。"""
|
||||
result = await db.execute(select(cls).filter(cls.id == user_id))
|
||||
return result.scalars().first()
|
||||
|
||||
def delete_by_name(self, db: Session, name: str):
|
||||
user = self.get_by_name(db, name)
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import legacy_async_db_query, legacy_db_query
|
||||
|
||||
|
||||
class Workflow(Base):
|
||||
@@ -56,18 +55,15 @@ class Workflow(Base):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_enabled_workflows(cls, db):
|
||||
return list(db.execute(select(cls).where(cls.state != 'P')).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_enabled_workflows(cls, db: AsyncSession):
|
||||
result = await db.execute(select(cls).where(cls.state != 'P'))
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_timer_triggered_workflows(cls, db):
|
||||
"""获取定时触发的工作流"""
|
||||
return list(db.execute(select(cls).where(
|
||||
@@ -81,7 +77,6 @@ class Workflow(Base):
|
||||
)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_timer_triggered_workflows(cls, db: AsyncSession):
|
||||
"""异步获取定时触发的工作流"""
|
||||
result = await db.execute(select(cls).where(
|
||||
@@ -96,7 +91,6 @@ class Workflow(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_event_triggered_workflows(cls, db):
|
||||
"""获取事件触发的工作流"""
|
||||
return list(db.execute(select(cls).where(
|
||||
@@ -107,7 +101,6 @@ class Workflow(Base):
|
||||
)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_event_triggered_workflows(cls, db: AsyncSession):
|
||||
"""异步获取事件触发的工作流"""
|
||||
result = await db.execute(select(cls).where(
|
||||
@@ -119,12 +112,10 @@ class Workflow(Base):
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
@legacy_db_query
|
||||
def get_by_name(cls, db, name: str):
|
||||
return db.execute(select(cls).where(cls.name == name)).scalars().first()
|
||||
|
||||
@classmethod
|
||||
@legacy_async_db_query
|
||||
async def async_get_by_name(cls, db: AsyncSession, name: str):
|
||||
result = await db.execute(select(cls).where(cls.name == name))
|
||||
return result.scalars().first()
|
||||
|
||||
@@ -321,6 +321,10 @@ class AgentChatOper(DbOper):
|
||||
await self._stage_async_delete(AgentChat, chat.id)
|
||||
return True
|
||||
|
||||
def delete_by_id(self, chat_id: int) -> None:
|
||||
"""在 Oper 事务边界内按主键删除 Agent 会话。"""
|
||||
self._stage_delete(AgentChat, chat_id)
|
||||
|
||||
async def async_stage_delete(
|
||||
self,
|
||||
session_id: str,
|
||||
|
||||
@@ -17,10 +17,12 @@ class DownloadFailureOper(DbOper):
|
||||
"""
|
||||
批量按指纹查询仍在冷却期的失败记录。
|
||||
"""
|
||||
failures = DownloadFailure.get_active_by_fingerprints(
|
||||
self._db,
|
||||
fingerprints=fingerprints,
|
||||
now_time=now_time,
|
||||
failures = self._execute_sync_query(
|
||||
lambda session: DownloadFailure.get_active_by_fingerprints(
|
||||
session,
|
||||
fingerprints=fingerprints,
|
||||
now_time=now_time,
|
||||
)
|
||||
)
|
||||
return {
|
||||
failure.fingerprint: failure
|
||||
@@ -38,12 +40,14 @@ class DownloadFailureOper(DbOper):
|
||||
"""
|
||||
新增或更新资源失败记录。
|
||||
"""
|
||||
return DownloadFailure.record_failure(
|
||||
self._db,
|
||||
fingerprint=fingerprint,
|
||||
now_time=now_time,
|
||||
next_retry_at=next_retry_at,
|
||||
**kwargs,
|
||||
return self._execute_sync_write(
|
||||
lambda session: DownloadFailure.record_failure(
|
||||
session,
|
||||
fingerprint=fingerprint,
|
||||
now_time=now_time,
|
||||
next_retry_at=next_retry_at,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def delete_expired(
|
||||
|
||||
@@ -289,7 +289,7 @@ class DownloadHistoryOper(DbOper):
|
||||
self._stage_delete(DownloadHistory, historyid)
|
||||
|
||||
def stage_delete_history(self, historyid: int) -> None:
|
||||
"""暂存下载记录删除,不由模型装饰器提交事务。"""
|
||||
"""暂存下载记录删除,事务由调用方统一提交。"""
|
||||
self._db.execute(
|
||||
sqlalchemy_delete(DownloadHistory).where(
|
||||
DownloadHistory.id == historyid
|
||||
|
||||
+35
-11
@@ -19,7 +19,11 @@ class PluginDataOper(DbOper):
|
||||
:param key: 数据key
|
||||
:param value: 数据值
|
||||
"""
|
||||
plugin = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
|
||||
plugin = self._execute_sync_query(
|
||||
lambda session: PluginData.get_plugin_data_by_key(
|
||||
session, plugin_id, key
|
||||
)
|
||||
)
|
||||
if plugin:
|
||||
self._stage_update(plugin, {
|
||||
"value": value
|
||||
@@ -35,8 +39,10 @@ class PluginDataOper(DbOper):
|
||||
:param key: 数据键
|
||||
:param value: 数据值
|
||||
"""
|
||||
plugin = await PluginData.async_get_plugin_data_by_key(
|
||||
self._db, plugin_id, key
|
||||
plugin = await self._execute_async_query(
|
||||
lambda session: PluginData.async_get_plugin_data_by_key(
|
||||
session, plugin_id, key
|
||||
)
|
||||
)
|
||||
if plugin:
|
||||
await self._stage_async_update(plugin, {"value": value})
|
||||
@@ -52,12 +58,18 @@ class PluginDataOper(DbOper):
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
data = PluginData.get_plugin_data_by_key(self._db, plugin_id, key)
|
||||
data = self._execute_sync_query(
|
||||
lambda session: PluginData.get_plugin_data_by_key(
|
||||
session, plugin_id, key
|
||||
)
|
||||
)
|
||||
if not data:
|
||||
return None
|
||||
return data.value
|
||||
else:
|
||||
return PluginData.get_plugin_data(self._db, plugin_id)
|
||||
return self._execute_sync_query(
|
||||
lambda session: PluginData.get_plugin_data(session, plugin_id)
|
||||
)
|
||||
|
||||
async def async_get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
|
||||
"""
|
||||
@@ -66,13 +78,17 @@ class PluginDataOper(DbOper):
|
||||
:param key: 数据key
|
||||
"""
|
||||
if key:
|
||||
data = await PluginData.async_get_plugin_data_by_key(
|
||||
self._db, plugin_id, key
|
||||
data = await self._execute_async_query(
|
||||
lambda session: PluginData.async_get_plugin_data_by_key(
|
||||
session, plugin_id, key
|
||||
)
|
||||
)
|
||||
if not data:
|
||||
return None
|
||||
return data.value
|
||||
return await PluginData.async_get_plugin_data(self._db, plugin_id)
|
||||
return await self._execute_async_query(
|
||||
lambda session: PluginData.async_get_plugin_data(session, plugin_id)
|
||||
)
|
||||
|
||||
def del_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
|
||||
"""
|
||||
@@ -81,7 +97,7 @@ class PluginDataOper(DbOper):
|
||||
:param key: 数据key
|
||||
"""
|
||||
def stage(session: Session) -> None:
|
||||
"""把兼容删除入口映射到调用方或组合根持有的事务。"""
|
||||
"""把删除入口映射到调用方或组合根持有的事务。"""
|
||||
if key:
|
||||
PluginData.del_plugin_data_by_key(session, plugin_id, key)
|
||||
else:
|
||||
@@ -109,11 +125,19 @@ class PluginDataOper(DbOper):
|
||||
获取插件所有数据
|
||||
:param plugin_id: 插件id
|
||||
"""
|
||||
return PluginData.get_plugin_data_by_plugin_id(self._db, plugin_id)
|
||||
return self._execute_sync_query(
|
||||
lambda session: PluginData.get_plugin_data_by_plugin_id(
|
||||
session, plugin_id
|
||||
)
|
||||
)
|
||||
|
||||
async def async_get_data_all(self, plugin_id: str) -> Any:
|
||||
"""
|
||||
异步获取插件所有数据。
|
||||
:param plugin_id: 插件id
|
||||
"""
|
||||
return await PluginData.async_get_plugin_data_by_plugin_id(self._db, plugin_id)
|
||||
return await self._execute_async_query(
|
||||
lambda session: PluginData.async_get_plugin_data_by_plugin_id(
|
||||
session, plugin_id
|
||||
)
|
||||
)
|
||||
|
||||
+50
-45
@@ -75,7 +75,7 @@ class SiteOper(DbOper):
|
||||
site_id: int,
|
||||
payload: Mapping[str, Any],
|
||||
) -> bool:
|
||||
"""暂存站点字段更新,不由模型装饰器提前提交。"""
|
||||
"""暂存站点字段更新,事务由调用方统一提交。"""
|
||||
site = await self.async_get(site_id)
|
||||
if not site:
|
||||
return False
|
||||
@@ -338,18 +338,22 @@ class SiteOper(DbOper):
|
||||
|
||||
async def async_get_icon_by_domain(self, domain: str) -> Optional[SiteIcon]:
|
||||
"""异步按域名获取站点图标。"""
|
||||
return await SiteIcon.async_get_by_domain(self._db, domain)
|
||||
return await self._execute_async_query(
|
||||
lambda session: SiteIcon.async_get_by_domain(session, domain)
|
||||
)
|
||||
|
||||
async def async_get_statistic_by_domain(
|
||||
self,
|
||||
domain: str,
|
||||
) -> Optional[SiteStatistic]:
|
||||
"""异步按域名获取站点统计。"""
|
||||
return await SiteStatistic.async_get_by_domain(self._db, domain)
|
||||
return await self._execute_async_query(
|
||||
lambda session: SiteStatistic.async_get_by_domain(session, domain)
|
||||
)
|
||||
|
||||
async def async_list_statistics(self) -> List[SiteStatistic]:
|
||||
"""异步获取所有站点统计。"""
|
||||
return await SiteStatistic.async_list(self._db)
|
||||
return await self._execute_async_query(SiteStatistic.async_list)
|
||||
|
||||
def get_userdata_by_date(self, date: str) -> List[SiteUserData]:
|
||||
"""
|
||||
@@ -371,7 +375,9 @@ class SiteOper(DbOper):
|
||||
"""
|
||||
按域名获取站点图标
|
||||
"""
|
||||
return SiteIcon.get_by_domain(self._db, domain)
|
||||
return self._execute_sync_query(
|
||||
lambda session: SiteIcon.get_by_domain(session, domain)
|
||||
)
|
||||
|
||||
def update_icon(self, name: str, domain: str, icon_url: str, icon_base64: str) -> bool:
|
||||
"""
|
||||
@@ -467,60 +473,59 @@ class SiteOper(DbOper):
|
||||
"""
|
||||
异步站点访问成功
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
# 使用深复制确保 note 是全新的字典对象
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
|
||||
avg_seconds = sum([v for v in note.values()]) // avg_times
|
||||
|
||||
await self._stage_async_update(sta, {
|
||||
"success": sta.success + 1,
|
||||
"seconds": avg_seconds or sta.seconds,
|
||||
"lst_state": 0,
|
||||
"lst_mod_date": lst_date,
|
||||
"note": note
|
||||
})
|
||||
else:
|
||||
note = {}
|
||||
if seconds is not None:
|
||||
note = {
|
||||
lst_date: seconds or 1
|
||||
}
|
||||
await self._stage_async_create(SiteStatistic(
|
||||
async def write(session: AsyncSession) -> None:
|
||||
"""在同一异步事务中读取并更新站点成功统计。"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(session, domain)
|
||||
if sta:
|
||||
note = dict(sta.note) if sta.note else {}
|
||||
avg_seconds = None
|
||||
if seconds is not None:
|
||||
note[lst_date] = seconds or 1
|
||||
avg_times = len(note.keys())
|
||||
if avg_times > 10:
|
||||
note = dict(sorted(
|
||||
note.items(), key=lambda item: item[0], reverse=True
|
||||
)[:10])
|
||||
avg_seconds = sum(note.values()) // avg_times
|
||||
sta.success += 1
|
||||
sta.seconds = avg_seconds or sta.seconds
|
||||
sta.lst_state = 0
|
||||
sta.lst_mod_date = lst_date
|
||||
sta.note = note
|
||||
return
|
||||
note = {lst_date: seconds or 1} if seconds is not None else {}
|
||||
session.add(SiteStatistic(
|
||||
domain=domain,
|
||||
success=1,
|
||||
fail=0,
|
||||
seconds=seconds or 1,
|
||||
lst_state=0,
|
||||
lst_mod_date=lst_date,
|
||||
note=note
|
||||
note=note,
|
||||
))
|
||||
|
||||
await self._execute_async_write(write)
|
||||
|
||||
async def async_fail(self, domain: str):
|
||||
"""
|
||||
异步站点访问失败
|
||||
"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
|
||||
if sta:
|
||||
await self._stage_async_update(sta, {
|
||||
"fail": sta.fail + 1,
|
||||
"lst_state": 1,
|
||||
"lst_mod_date": lst_date
|
||||
})
|
||||
else:
|
||||
await self._stage_async_create(SiteStatistic(
|
||||
async def write(session: AsyncSession) -> None:
|
||||
"""在同一异步事务中读取并更新站点失败统计。"""
|
||||
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sta = await SiteStatistic.async_get_by_domain(session, domain)
|
||||
if sta:
|
||||
sta.fail += 1
|
||||
sta.lst_state = 1
|
||||
sta.lst_mod_date = lst_date
|
||||
return
|
||||
session.add(SiteStatistic(
|
||||
domain=domain,
|
||||
success=0,
|
||||
fail=1,
|
||||
lst_state=1,
|
||||
lst_mod_date=lst_date
|
||||
lst_mod_date=lst_date,
|
||||
))
|
||||
|
||||
await self._execute_async_write(write)
|
||||
|
||||
+90
-163
@@ -11,7 +11,7 @@ import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Tuple, List, Optional
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete, select
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -100,25 +100,6 @@ class SubscribeOper(DbOper):
|
||||
订阅管理
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _identity_statement(identity: dict, username: Optional[str] = None):
|
||||
"""构造订阅查重语句,SQL 所有权收口在 Oper。"""
|
||||
condition = Subscribe._identity_condition( # pylint: disable=protected-access
|
||||
identity.get("media_source"),
|
||||
identity.get("media_id"),
|
||||
identity.get("music_type"),
|
||||
)
|
||||
if condition is None or username == "":
|
||||
return None
|
||||
statement = select(Subscribe).where(condition)
|
||||
if username:
|
||||
statement = statement.where(Subscribe.username == username)
|
||||
if identity.get("season") is not None:
|
||||
statement = statement.where(Subscribe.season == identity["season"])
|
||||
return statement.where(
|
||||
Subscribe.episode_group == identity.get("episode_group")
|
||||
)
|
||||
|
||||
def _exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
|
||||
"""
|
||||
按身份查重。
|
||||
@@ -126,19 +107,19 @@ class SubscribeOper(DbOper):
|
||||
:param username: 非空时只在该用户的订阅内查
|
||||
:return: 命中的订阅行,未命中为 None
|
||||
"""
|
||||
if isinstance(self._db, Session):
|
||||
statement = self._identity_statement(identity, username)
|
||||
if statement is None:
|
||||
return None
|
||||
return self._db.execute(statement).scalars().first()
|
||||
# 旧 SDK 允许无会话构造 Oper;保留其自动短会话行为,但规范入口不得走这里。
|
||||
if username == "":
|
||||
return None
|
||||
if username:
|
||||
return Subscribe.exists_by_username(
|
||||
self._db,
|
||||
username=username,
|
||||
**identity,
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.exists_by_username(
|
||||
session,
|
||||
username=username,
|
||||
**identity,
|
||||
)
|
||||
)
|
||||
return Subscribe.exists(self._db, **identity)
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.exists(session, **identity)
|
||||
)
|
||||
|
||||
async def _async_exists(self, identity: dict, username: Optional[str]) -> Optional[Any]:
|
||||
"""
|
||||
@@ -147,20 +128,18 @@ class SubscribeOper(DbOper):
|
||||
:param username: 非空时只在该用户的订阅内查
|
||||
:return: 命中的订阅行,未命中为 None
|
||||
"""
|
||||
if isinstance(self._db, AsyncSession):
|
||||
statement = self._identity_statement(identity, username)
|
||||
if statement is None:
|
||||
async def query(session: AsyncSession) -> Optional[Subscribe]:
|
||||
"""在调用方或组合根异步会话中执行订阅查重。"""
|
||||
if username == "":
|
||||
return None
|
||||
result = await self._db.execute(statement)
|
||||
return result.scalars().first()
|
||||
# 同步路径一样只为无会话旧入口保留 Model 的自动短会话兼容。
|
||||
if username:
|
||||
return await Subscribe.async_exists_by_username(
|
||||
self._db,
|
||||
username=username,
|
||||
**identity,
|
||||
)
|
||||
return await Subscribe.async_exists(self._db, **identity)
|
||||
if username:
|
||||
return await Subscribe.async_exists_by_username(
|
||||
session,
|
||||
username=username,
|
||||
**identity,
|
||||
)
|
||||
return await Subscribe.async_exists(session, **identity)
|
||||
return await self._execute_async_query(query)
|
||||
|
||||
def stage_add(
|
||||
self,
|
||||
@@ -297,24 +276,15 @@ class SubscribeOper(DbOper):
|
||||
"""
|
||||
获取订阅
|
||||
"""
|
||||
return self._execute_sync_query(
|
||||
lambda session: session.execute(
|
||||
select(Subscribe).where(Subscribe.id == sid)
|
||||
).scalars().first()
|
||||
)
|
||||
return self._execute_sync_query(lambda session: Subscribe.get(session, sid))
|
||||
|
||||
async def async_get(self, sid: int) -> Optional[Subscribe]:
|
||||
"""
|
||||
获取订阅
|
||||
"""
|
||||
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
|
||||
# 保留旧测试替身与插件注入对象对 Model ABI 的兼容入口。
|
||||
return await Subscribe.async_get(self._db, rid=sid)
|
||||
async def query(session: AsyncSession) -> Optional[Subscribe]:
|
||||
"""在调用方异步会话中执行订阅主键查询。"""
|
||||
result = await session.execute(select(Subscribe).where(Subscribe.id == sid))
|
||||
return result.scalars().first()
|
||||
return await self._execute_async_query(query)
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_get(session, sid)
|
||||
)
|
||||
|
||||
async def async_list_by_media_identity(
|
||||
self,
|
||||
@@ -323,18 +293,14 @@ class SubscribeOper(DbOper):
|
||||
music_type: Optional[str] = None,
|
||||
) -> List[Subscribe]:
|
||||
"""异步按规范媒体身份读取订阅。"""
|
||||
async def query(session: AsyncSession) -> List[Subscribe]:
|
||||
"""在调用方异步会话中执行媒体身份列表查询。"""
|
||||
condition = Subscribe._identity_condition( # pylint: disable=protected-access
|
||||
media_source, media_id, music_type
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_list_by_media_identity(
|
||||
session,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
music_type=music_type,
|
||||
)
|
||||
if condition is None:
|
||||
return []
|
||||
result = await session.execute(select(Subscribe).where(condition))
|
||||
return list(result.scalars().all())
|
||||
if isinstance(self._db, AsyncSession):
|
||||
return await query(self._db)
|
||||
return await self._execute_async_query(query)
|
||||
)
|
||||
|
||||
def list_by_media_identity(
|
||||
self,
|
||||
@@ -343,15 +309,14 @@ class SubscribeOper(DbOper):
|
||||
music_type: Optional[str] = None,
|
||||
) -> List[Subscribe]:
|
||||
"""同步按规范媒体身份读取订阅。"""
|
||||
def query(session: Session) -> List[Subscribe]:
|
||||
"""在调用方同步会话中执行媒体身份列表查询。"""
|
||||
condition = Subscribe._identity_condition( # pylint: disable=protected-access
|
||||
media_source, media_id, music_type
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.list_by_media_identity(
|
||||
session,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
music_type=music_type,
|
||||
)
|
||||
if condition is None:
|
||||
return []
|
||||
return list(session.execute(select(Subscribe).where(condition)).scalars().all())
|
||||
return self._execute_sync_query(query)
|
||||
)
|
||||
|
||||
async def get_candidate(
|
||||
self,
|
||||
@@ -423,18 +388,16 @@ class SubscribeOper(DbOper):
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
def query(session: Session) -> Optional[Subscribe]:
|
||||
"""在调用方同步会话中执行类型媒体查询。"""
|
||||
condition = Subscribe._identity_condition( # pylint: disable=protected-access
|
||||
media_source, media_id, music_type
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.get_by(
|
||||
session,
|
||||
type=type,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
season=season,
|
||||
music_type=music_type,
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
statement = select(Subscribe).where(condition, Subscribe.type == type)
|
||||
if season is not None:
|
||||
statement = statement.where(Subscribe.season == season)
|
||||
return session.execute(statement).scalars().first()
|
||||
return self._execute_sync_query(query)
|
||||
)
|
||||
|
||||
async def async_get_by(
|
||||
self, type: str, media_source: MediaSource, media_id: str,
|
||||
@@ -444,55 +407,34 @@ class SubscribeOper(DbOper):
|
||||
"""
|
||||
根据条件查询订阅
|
||||
"""
|
||||
async def query(session: AsyncSession) -> Optional[Subscribe]:
|
||||
"""在调用方异步会话中执行类型媒体查询。"""
|
||||
condition = Subscribe._identity_condition( # pylint: disable=protected-access
|
||||
media_source, media_id, music_type
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_get_by(
|
||||
session,
|
||||
type=type,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
season=season,
|
||||
music_type=music_type,
|
||||
)
|
||||
if condition is None:
|
||||
return None
|
||||
statement = select(Subscribe).where(condition, Subscribe.type == type)
|
||||
if season is not None:
|
||||
statement = statement.where(Subscribe.season == season)
|
||||
result = await session.execute(statement)
|
||||
return result.scalars().first()
|
||||
return await self._execute_async_query(query)
|
||||
)
|
||||
|
||||
def list(self, state: Optional[str] = None) -> List[Subscribe]:
|
||||
"""
|
||||
获取订阅列表
|
||||
"""
|
||||
if state:
|
||||
return self._execute_sync_query(
|
||||
lambda session: list(session.execute(
|
||||
select(Subscribe).where(Subscribe.state.in_(state.split(',')))
|
||||
).scalars().all())
|
||||
)
|
||||
return self._execute_sync_query(
|
||||
lambda session: list(session.execute(select(Subscribe)).scalars().all())
|
||||
lambda session: Subscribe.get_by_state(session, state)
|
||||
)
|
||||
|
||||
async def async_list(self, state: Optional[str] = None) -> List[Subscribe]:
|
||||
"""
|
||||
异步获取订阅列表
|
||||
"""
|
||||
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
|
||||
if state:
|
||||
return await Subscribe.async_get_by_state(self._db, state)
|
||||
return await Subscribe.async_list(self._db)
|
||||
if state:
|
||||
async def query(session: AsyncSession) -> List[Subscribe]:
|
||||
"""在调用方异步会话中执行状态列表查询。"""
|
||||
result = await session.execute(
|
||||
select(Subscribe).where(Subscribe.state.in_(state.split(',')))
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
return await self._execute_async_query(query)
|
||||
async def query_all(session: AsyncSession) -> List[Subscribe]:
|
||||
"""在调用方异步会话中执行全量订阅查询。"""
|
||||
result = await session.execute(select(Subscribe))
|
||||
return list(result.scalars().all())
|
||||
return await self._execute_async_query(query_all)
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_get_by_state(session, state)
|
||||
)
|
||||
return await self._execute_async_query(Subscribe.async_list)
|
||||
|
||||
async def async_list_by_username(
|
||||
self,
|
||||
@@ -501,35 +443,28 @@ class SubscribeOper(DbOper):
|
||||
mtype: Optional[str] = None,
|
||||
) -> List[Subscribe]:
|
||||
"""异步按用户获取订阅。"""
|
||||
if self._db is not None and not isinstance(self._db, (Session, AsyncSession)):
|
||||
return await Subscribe.async_list_by_username(
|
||||
self._db, username=username, state=state, mtype=mtype
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_list_by_username(
|
||||
session,
|
||||
username=username,
|
||||
state=state,
|
||||
mtype=mtype,
|
||||
)
|
||||
async def query(session: AsyncSession) -> List[Subscribe]:
|
||||
"""在调用方异步会话中执行用户筛选查询。"""
|
||||
statement = select(Subscribe).where(Subscribe.username == username)
|
||||
if state:
|
||||
statement = statement.where(Subscribe.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(Subscribe.type == mtype)
|
||||
result = await session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
return await self._execute_async_query(query)
|
||||
)
|
||||
|
||||
async def async_list_by_title(
|
||||
self,
|
||||
title: str,
|
||||
season: Optional[int] = None,
|
||||
) -> List[Subscribe]:
|
||||
"""异步按标题获取订阅,供旧查询测试和迁移调用兼容。"""
|
||||
async def query(session: AsyncSession) -> List[Subscribe]:
|
||||
"""在调用方异步会话中执行标题列表查询。"""
|
||||
statement = select(Subscribe).where(Subscribe.name == title)
|
||||
if season is not None:
|
||||
statement = statement.where(Subscribe.season == season)
|
||||
result = await session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
return await self._execute_async_query(query)
|
||||
"""在 Oper 会话边界内异步按标题获取订阅。"""
|
||||
return await self._execute_async_query(
|
||||
lambda session: Subscribe.async_list_by_title(
|
||||
session,
|
||||
title=title,
|
||||
season=season,
|
||||
)
|
||||
)
|
||||
|
||||
def delete(self, sid: int):
|
||||
"""
|
||||
@@ -598,30 +533,22 @@ class SubscribeOper(DbOper):
|
||||
"""
|
||||
获取指定用户的订阅
|
||||
"""
|
||||
def query(session: Session) -> List[Subscribe]:
|
||||
"""在调用方同步会话中执行用户筛选查询。"""
|
||||
statement = select(Subscribe).where(Subscribe.username == username)
|
||||
if state:
|
||||
statement = statement.where(Subscribe.state == state)
|
||||
if mtype:
|
||||
statement = statement.where(Subscribe.type == mtype)
|
||||
return list(session.execute(statement).scalars().all())
|
||||
return self._execute_sync_query(query)
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.list_by_username(
|
||||
session,
|
||||
username=username,
|
||||
state=state,
|
||||
mtype=mtype,
|
||||
)
|
||||
)
|
||||
|
||||
def list_by_type(self, mtype: str, days: int = 7) -> List[Subscribe]:
|
||||
"""
|
||||
获取指定类型的订阅
|
||||
"""
|
||||
def query(session: Session) -> List[Subscribe]:
|
||||
"""在调用方同步会话中执行时间窗订阅查询。"""
|
||||
cutoff = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
time.localtime(time.time() - 86400 * int(days)),
|
||||
)
|
||||
return list(session.execute(select(Subscribe).where(
|
||||
Subscribe.type == mtype, Subscribe.date >= cutoff
|
||||
)).scalars().all())
|
||||
return self._execute_sync_query(query)
|
||||
return self._execute_sync_query(
|
||||
lambda session: Subscribe.list_by_type(session, mtype, days)
|
||||
)
|
||||
|
||||
def add_history(self, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,8 @@ import copy
|
||||
import threading
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.systemconfig import SystemConfig
|
||||
from app.schemas.types import SystemConfigKey
|
||||
@@ -20,12 +22,15 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
|
||||
self._write_lock = threading.RLock()
|
||||
self._loaded = False
|
||||
|
||||
def load_snapshot(self) -> None:
|
||||
"""从数据库加载完整配置,并一次性发布新的内存快照。"""
|
||||
def load_snapshot(self, db: Optional[Session] = None) -> None:
|
||||
"""从显式会话或 Oper 事务边界加载配置并发布内存快照。"""
|
||||
with self._write_lock:
|
||||
items = SystemConfig.list(db) if db is not None else self._execute_sync_query(
|
||||
SystemConfig.list
|
||||
)
|
||||
snapshot = {
|
||||
item.key: copy.deepcopy(item.value)
|
||||
for item in SystemConfig.list(self._db)
|
||||
for item in items
|
||||
}
|
||||
with self._snapshot_lock:
|
||||
self.__SYSTEMCONF = snapshot
|
||||
|
||||
@@ -269,7 +269,7 @@ class TransferHistoryOper(DbOper):
|
||||
self._stage_delete(TransferHistory, historyid)
|
||||
|
||||
def stage_delete(self, historyid: int) -> None:
|
||||
"""暂存整理记录删除,不由模型装饰器提交事务。"""
|
||||
"""暂存整理记录删除,事务由调用方统一提交。"""
|
||||
self._db.execute(
|
||||
sqlalchemy_delete(TransferHistory).where(
|
||||
TransferHistory.id == historyid
|
||||
|
||||
+10
-6
@@ -27,7 +27,7 @@ class UserOper(DbOper):
|
||||
"""
|
||||
获取用户列表
|
||||
"""
|
||||
return User.list(self._db)
|
||||
return self._execute_sync_query(User.list)
|
||||
|
||||
def add(self, **kwargs):
|
||||
"""
|
||||
@@ -40,15 +40,19 @@ class UserOper(DbOper):
|
||||
"""
|
||||
根据用户名获取用户
|
||||
"""
|
||||
return User.get_by_name(self._db, name)
|
||||
return self._execute_sync_query(
|
||||
lambda session: User.get_by_name(session, name)
|
||||
)
|
||||
|
||||
def get_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""按 ID 获取用户。"""
|
||||
return User.get_by_id(self._db, user_id)
|
||||
return self._execute_sync_query(
|
||||
lambda session: User.get_by_id(session, user_id)
|
||||
)
|
||||
|
||||
async def async_list(self) -> List[User]:
|
||||
"""异步获取用户列表。"""
|
||||
return await User.async_list(self._db)
|
||||
return await self._execute_async_query(User.async_list)
|
||||
|
||||
async def async_create(self, payload: dict) -> Optional[User]:
|
||||
"""异步创建用户。"""
|
||||
@@ -126,7 +130,7 @@ class UserOper(DbOper):
|
||||
"""
|
||||
获取用户权限
|
||||
"""
|
||||
user = User.get_by_name(self._db, name)
|
||||
user = self.get_by_name(name)
|
||||
if user:
|
||||
return user.permissions or {}
|
||||
return {}
|
||||
@@ -135,7 +139,7 @@ class UserOper(DbOper):
|
||||
"""
|
||||
获取用户个性化设置,返回None表示用户不存在
|
||||
"""
|
||||
user = User.get_by_name(self._db, name)
|
||||
user = self.get_by_name(name)
|
||||
if user:
|
||||
return user.settings or {}
|
||||
return None
|
||||
|
||||
@@ -2,6 +2,8 @@ import copy
|
||||
import threading
|
||||
from typing import Any, Union, Dict, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.userconfig import UserConfig
|
||||
from app.schemas.types import UserConfigKey
|
||||
@@ -20,11 +22,14 @@ class UserConfigOper(DbOper, metaclass=Singleton):
|
||||
self._write_lock = threading.RLock()
|
||||
self._loaded = False
|
||||
|
||||
def load_snapshot(self) -> None:
|
||||
"""从数据库加载完整用户配置,并一次性发布新的内存快照。"""
|
||||
def load_snapshot(self, db: Optional[Session] = None) -> None:
|
||||
"""从显式会话或 Oper 事务边界加载用户配置并发布内存快照。"""
|
||||
with self._write_lock:
|
||||
snapshot: dict[str, dict[str, Any]] = {}
|
||||
for item in UserConfig.list(self._db):
|
||||
items = UserConfig.list(db) if db is not None else self._execute_sync_query(
|
||||
UserConfig.list
|
||||
)
|
||||
for item in items:
|
||||
if item.username and item.key:
|
||||
snapshot.setdefault(item.username, {})[item.key] = copy.deepcopy(
|
||||
item.value
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import List, Mapping, Tuple, Optional, Any, Protocol
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.base import DbOper
|
||||
from app.db.models.workflow import Workflow
|
||||
@@ -202,6 +203,8 @@ class WorkflowOper(DbOper):
|
||||
|
||||
def stage_start(self, wid: int) -> bool:
|
||||
"""在调用方持有的会话中暂存运行中状态。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
|
||||
return Workflow.start(self._db, wid)
|
||||
|
||||
def success(self, wid: int, result: Optional[str] = None) -> bool:
|
||||
@@ -214,6 +217,8 @@ class WorkflowOper(DbOper):
|
||||
|
||||
def stage_success(self, wid: int, result: Optional[str] = None) -> bool:
|
||||
"""在调用方持有的会话中暂存成功状态。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
|
||||
return Workflow.success(self._db, wid, result)
|
||||
|
||||
def fail(self, wid: int, result: str) -> bool:
|
||||
@@ -226,6 +231,8 @@ class WorkflowOper(DbOper):
|
||||
|
||||
def stage_fail(self, wid: int, result: str) -> bool:
|
||||
"""在调用方持有的会话中暂存失败状态。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
|
||||
return Workflow.fail(self._db, wid, result)
|
||||
|
||||
def step(
|
||||
@@ -255,6 +262,8 @@ class WorkflowOper(DbOper):
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""在调用方持有的会话中暂存动作进度。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
|
||||
return Workflow.update_current_action(
|
||||
self._db,
|
||||
wid,
|
||||
@@ -277,4 +286,6 @@ class WorkflowOper(DbOper):
|
||||
reset_count: bool = False,
|
||||
) -> bool:
|
||||
"""在调用方持有的会话中暂存执行状态重置。"""
|
||||
if not isinstance(self._db, Session):
|
||||
raise RuntimeError("工作流暂存写入需要调用方提供同步 Session")
|
||||
return Workflow.reset(self._db, wid, reset_count=reset_count)
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
"""SQLAlchemy 请求级事务适配器与旧 Oper 事务执行端口。"""
|
||||
"""SQLAlchemy 请求级事务适配器与无会话 Oper 事务执行端口。"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Protocol, TypeVar
|
||||
@@ -11,7 +11,7 @@ T = TypeVar("T")
|
||||
|
||||
|
||||
class SyncTransactionRunner(Protocol):
|
||||
"""为无显式 Session 的兼容写入口提供独占同步事务。"""
|
||||
"""为无显式 Session 的 Oper 入口提供独占同步事务。"""
|
||||
|
||||
def __call__(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在一个独占会话中执行并提交操作。"""
|
||||
@@ -19,7 +19,7 @@ class SyncTransactionRunner(Protocol):
|
||||
|
||||
|
||||
class AsyncTransactionRunner(Protocol):
|
||||
"""为无显式 Session 的兼容写入口提供独占异步事务。"""
|
||||
"""为无显式 Session 的 Oper 入口提供独占异步事务。"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
@@ -38,14 +38,14 @@ def configure_transaction_runners(
|
||||
sync: SyncTransactionRunner,
|
||||
async_: AsyncTransactionRunner,
|
||||
) -> None:
|
||||
"""由组合根登记旧 Oper 兼容入口使用的显式事务执行器。"""
|
||||
"""由组合根登记无会话 Oper 入口使用的显式事务执行器。"""
|
||||
global _sync_transaction_runner, _async_transaction_runner
|
||||
_sync_transaction_runner = sync
|
||||
_async_transaction_runner = async_
|
||||
|
||||
|
||||
def run_sync_transaction(operation: Callable[[Session], T]) -> T:
|
||||
"""委托组合根在独占同步事务中执行兼容写操作。"""
|
||||
"""委托组合根在独占同步事务中执行 Oper 操作。"""
|
||||
if _sync_transaction_runner is None:
|
||||
raise RuntimeError("同步事务执行器尚未配置")
|
||||
return _sync_transaction_runner(operation)
|
||||
@@ -54,7 +54,7 @@ def run_sync_transaction(operation: Callable[[Session], T]) -> T:
|
||||
async def run_async_transaction(
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""委托组合根在独占异步事务中执行兼容写操作。"""
|
||||
"""委托组合根在独占异步事务中执行 Oper 操作。"""
|
||||
if _async_transaction_runner is None:
|
||||
raise RuntimeError("异步事务执行器尚未配置")
|
||||
return await _async_transaction_runner(operation)
|
||||
|
||||
Reference in New Issue
Block a user