diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 4ad0f5f5..6a8b07fd 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -19,6 +19,7 @@ from app.db.models.user import User from app.db.systemconfig_oper import SystemConfigOper from app.db.user_oper import get_current_active_user_async from app.helper.server import MoviePilotServerHelper +from app.log import logger from app.scheduler import Scheduler from app.schemas.types import MediaType, EventType, SystemConfigKey @@ -41,6 +42,14 @@ def start_subscribe_add( ) +def build_subscribe_event_payload(subscribe: Subscribe) -> dict: + """ + 从 ORM 已加载字段构造订阅事件快照,避免异步接口里属性懒加载触发隐式 IO。 + """ + values = subscribe.__dict__ + return {column.name: values.get(column.name) for column in subscribe.__table__.columns} + + @router.get("/", summary="查询所有订阅", response_model=List[schemas.Subscribe]) async def read_subscribes( db: AsyncSession = Depends(get_async_db), @@ -349,16 +358,27 @@ async def delete_subscribe_by_mediaid( subscribe = await Subscribe.async_get_by_mediaid(db, mediaid) if subscribe: delete_subscribes.append(subscribe) + delete_events = [] for subscribe in delete_subscribes: - # 在删除之前获取订阅信息 - subscribe_info = subscribe.to_dict() - subscribe_id = subscribe.id - await Subscribe.async_delete(db, subscribe_id) - # 发送事件 - await eventmanager.async_send_event( - EventType.SubscribeDeleted, - {"subscribe_id": subscribe_id, "subscribe_info": subscribe_info}, - ) + subscribe_info = build_subscribe_event_payload(subscribe) + subscribe_id = subscribe_info.get("id") + if not subscribe_id: + continue + delete_events.append((subscribe_id, subscribe_info)) + await db.delete(subscribe) + try: + await db.commit() + except Exception: + await db.rollback() + raise + for subscribe_id, subscribe_info in delete_events: + try: + await eventmanager.async_send_event( + EventType.SubscribeDeleted, + {"subscribe_id": subscribe_id, "subscribe_info": subscribe_info}, + ) + except Exception as err: + logger.error(f"发送订阅删除事件失败:{subscribe_id} - {err}", exc_info=True) return schemas.Response(success=True) @@ -726,8 +746,13 @@ async def delete_subscribe( subscribe = await Subscribe.async_get(db, subscribe_id) if subscribe: # 在删除之前获取订阅信息 - subscribe_info = subscribe.to_dict() - await Subscribe.async_delete(db, subscribe_id) + subscribe_info = build_subscribe_event_payload(subscribe) + await db.delete(subscribe) + try: + await db.commit() + except Exception: + await db.rollback() + raise # 发送事件 await eventmanager.async_send_event( EventType.SubscribeDeleted, @@ -735,6 +760,6 @@ async def delete_subscribe( ) # 统计订阅 MoviePilotServerHelper.sub_done_async( - {"tmdbid": subscribe.tmdbid, "doubanid": subscribe.doubanid} + {"tmdbid": subscribe_info.get("tmdbid"), "doubanid": subscribe_info.get("doubanid")} ) return schemas.Response(success=True) diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index cb88fc4b..ffb222a8 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -582,8 +582,8 @@ class SubscribeChain(ChainBase): "include") else kwargs.get("include"), 'exclude': self.__get_default_subscribe_config(mtype, "exclude") if not kwargs.get( "exclude") else kwargs.get("exclude"), - 'best_version': self.__get_default_subscribe_config(mtype, "best_version") if not kwargs.get( - "best_version") else kwargs.get("best_version"), + 'best_version': self.__get_default_subscribe_config(mtype, "best_version") + if kwargs.get("best_version") is None else kwargs.get("best_version"), 'best_version_full': self.__get_default_subscribe_config(mtype, "best_version_full") if kwargs.get("best_version_full") is None else kwargs.get("best_version_full"), 'search_imdbid': self.__get_default_subscribe_config(mtype, "search_imdbid") if not kwargs.get( diff --git a/app/schemas/subscribe.py b/app/schemas/subscribe.py index 3069c537..d1cf1bb5 100644 --- a/app/schemas/subscribe.py +++ b/app/schemas/subscribe.py @@ -62,9 +62,9 @@ class Subscribe(BaseModel): # 下载器 downloader: Optional[str] = None # 是否洗版 - best_version: Optional[int] = 0 + best_version: Optional[int] = None # 是否只洗全集整包 - best_version_full: Optional[int] = 0 + best_version_full: Optional[int] = None # 当前优先级 current_priority: Optional[int] = None # 洗版时已下载剧集的优先级状态 diff --git a/tests/test_subscribe_chain.py b/tests/test_subscribe_chain.py index 404eaa3c..1e50bf1d 100644 --- a/tests/test_subscribe_chain.py +++ b/tests/test_subscribe_chain.py @@ -372,6 +372,25 @@ class SubscribeChainTest(TestCase): media_info=SimpleNamespace(type=MediaType.TV, tmdb_id=1, douban_id=None), ) + def test_default_kwargs_respects_explicit_zero_best_version(self): + """显式关闭洗版时必须保留 0,仅未传值才应用默认订阅规则。""" + + def _default_config(_mtype, key): + return 1 if key in {"best_version", "best_version_full"} else None + + with patch.object(SubscribeChain, "_SubscribeChain__get_default_subscribe_config", side_effect=_default_config): + explicit = SubscribeChain()._SubscribeChain__get_default_kwargs( + MediaType.TV, + best_version=0, + best_version_full=0, + ) + omitted = SubscribeChain()._SubscribeChain__get_default_kwargs(MediaType.TV) + + self.assertEqual(explicit["best_version"], 0) + self.assertEqual(explicit["best_version_full"], 0) + self.assertEqual(omitted["best_version"], 1) + self.assertEqual(omitted["best_version_full"], 1) + def test_format_subscribe_progress_preserves_special_season_zero(self): """订阅列表展示必须把 S0 当作合法季号,而不是回退到第 1 季。""" subscribe = self._build_subscribe(season=0, total_episode=5, lack_episode=2)