Files
MoviePilot/app/db/oper/pluginidentity.py
Aqr-K 5e232ca14d fix(plugin): 安装记录大小写不敏感查询、游标物化,site 端点改从 Application 取 (#6682)
* fix(plugin): 安装 journal 按大小写不敏感列出事务记录

create 用 func.lower 判定同一物理插件并据此拒绝重复占位,list 却还是等值过滤。
判据分叉后,恢复流程查不到那条被 create 认定为未收尾的 journal,会认为没有待恢复
事务,而新事务又永远被同一行挡住。list 改用与 create 相同的 func.lower 判据。

* fix(plugin): 批量读插件身份时在事务内物化游标

list_by_plugin_ids 无会话调用时由组合根另开短事务,会话在 lambda 返回后立即关闭,
外层再 list() 消费惰性游标会撞上 "identity map is no longer valid",一行都读不到。
补上 .all(),让行在事务内取完。

* refactor(api): site 端点从 Application 取插件路由刷新

register_plugin_api 的唯一拥有者是 app.application.plugin.routes,
dependencies/plugin.py、scheduler/reconcile.py 与 initializers/plugins.py 都已从那里
取用,只剩 site 端点还在向 plugin 端点横向借。改为同一来源,并加门禁锁住这条判据。

依赖基线相应下降一条边(8650 -> 8649:site 不再到达 app.api.endpoints 与
app.api.endpoints.plugin,改为到达 app.application.plugin.routes),mypy 低水位同步
下降一条 attr-defined,两处文档数字随之更新。

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
2026-09-14 07:18:50 +08:00

84 lines
2.9 KiB
Python

"""插件来源身份的数据访问原语。"""
from collections.abc import Sequence
from typing import cast
from sqlalchemy import select, update
from sqlalchemy.orm import Session
from app.db.base import DbOper, execute_dml
from app.db.models.pluginidentity import PluginIdentity
class PluginIdentityOper(DbOper):
"""在调用方 Session 中查询并条件暂存插件来源身份。"""
def get_by_plugin_id(self, plugin_id: str) -> PluginIdentity | None:
"""按规范化物理插件 ID 查询唯一身份。"""
return self._execute_sync_query(
lambda session: cast(
PluginIdentity | None,
session.execute(
select(PluginIdentity).where(
PluginIdentity.normalized_plugin_id == plugin_id
)
).scalar_one_or_none(),
)
)
def list_by_plugin_ids(
self,
plugin_ids: Sequence[str],
) -> list[PluginIdentity]:
"""批量读取规范化物理插件 ID 对应的身份。"""
if not plugin_ids:
return []
# 无会话调用由组合根另开短事务,会话在 lambda 返回后立刻关闭;游标必须在事务
# 内物化,否则外层消费时身份映射已失效,读不到任何行
return list(
self._execute_sync_query(
lambda session: session.execute(
select(PluginIdentity).where(
PluginIdentity.normalized_plugin_id.in_(plugin_ids)
)
).scalars().all()
)
)
def stage_create(self, identity: PluginIdentity) -> None:
"""暂存首次身份并立即暴露数据库唯一键竞争。"""
def stage(session: Session) -> None:
"""加入并 flush 当前调用方事务。"""
session.add(identity)
session.flush()
self._execute_sync_write(stage)
def stage_replace(
self,
identity: PluginIdentity,
*,
expected_revision: int,
) -> bool:
"""仅在当前 revision 匹配时替换整份审计事实。"""
values = {
column.name: getattr(identity, column.name)
for column in PluginIdentity.__table__.columns
if column.name != "id"
}
return bool(
self._execute_sync_write(
lambda session: execute_dml(
session,
update(PluginIdentity)
.where(
PluginIdentity.normalized_plugin_id
== identity.normalized_plugin_id,
PluginIdentity.revision == expected_revision,
)
.values(**values),
execution_options={"synchronize_session": False},
)
)
)