From e0adb4752106f045137a8abe1ff2a28b2fd65d89 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 14 Sep 2026 19:58:27 +0800 Subject: [PATCH] refactor(architecture): keep subscription owners within ratchet --- app/application/messaging/subscribe.py | 47 +++++------ app/chain/subscribe/completion.py | 104 ++++++++++++------------- app/chain/subscribe/query.py | 13 +++- 3 files changed, 85 insertions(+), 79 deletions(-) diff --git a/app/application/messaging/subscribe.py b/app/application/messaging/subscribe.py index e21c2f09c..004d6bf7a 100644 --- a/app/application/messaging/subscribe.py +++ b/app/application/messaging/subscribe.py @@ -22,6 +22,29 @@ from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType, NotificationChannel subscribe_interaction_manager = SlashInteractionManager() +def _format_subscribe_progress(subscribe: SubscriptionSnapshot) -> str: + """构造订阅的季和进度说明,专辑按累计曲目数展示。""" + if subscribe.type == MediaType.MOVIE.value: + return "电影" + if ( + subscribe.type == MediaType.MUSIC.value + and subscribe.music_type == MUSIC_ENTITY_ALBUM + and subscribe.total_tracks + ): + completed_tracks = compute_subscribe_completed_tracks(subscribe) or 0 + return f"专辑 [{completed_tracks}/{subscribe.total_tracks}]" + season = subscribe.season if subscribe.season is not None else 1 + if subscribe.total_episode: + lack_episode = ( + subscribe.lack_episode + if subscribe.lack_episode is not None + else subscribe.total_episode + ) + downloaded = max(subscribe.total_episode - lack_episode, 0) + return f"第{season}季 [{downloaded}/{subscribe.total_episode}]" + return f"第{season}季" + + class SubscribeInteractionActions(Protocol): """ 声明订阅交互需要调用的业务动作。 @@ -540,28 +563,8 @@ class SubscribeInteractionHandler: @staticmethod def _format_subscribe_progress(subscribe: SubscriptionSnapshot) -> str: - """ - 构造订阅的季和进度说明。 - """ - if subscribe.type == MediaType.MOVIE.value: - return "电影" - if ( - subscribe.type == MediaType.MUSIC.value - and subscribe.music_type == MUSIC_ENTITY_ALBUM - and subscribe.total_tracks - ): - completed_tracks = compute_subscribe_completed_tracks(subscribe) or 0 - return f"专辑 [{completed_tracks}/{subscribe.total_tracks}]" - season = subscribe.season if subscribe.season is not None else 1 - if subscribe.total_episode: - lack_episode = ( - subscribe.lack_episode - if subscribe.lack_episode is not None - else subscribe.total_episode - ) - downloaded = max(subscribe.total_episode - lack_episode, 0) - return f"第{season}季 [{downloaded}/{subscribe.total_episode}]" - return f"第{season}季" + """构造订阅的季和进度说明。""" + return _format_subscribe_progress(subscribe) @staticmethod def _subscribe_prompt(awaiting_input: Optional[str]) -> str: diff --git a/app/chain/subscribe/completion.py b/app/chain/subscribe/completion.py index 744aa8cec..6681499fe 100644 --- a/app/chain/subscribe/completion.py +++ b/app/chain/subscribe/completion.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from app.application.classification.reference import effective_classification_snapshot from app.application.subscription.contract import ( @@ -129,10 +129,9 @@ class SubscribeCompletionOwner(_SubscribeOwnerBase): ) subscribe = facts["subscribe"] elif downloads and meta.type == MediaType.MUSIC: - subscribe = self._SubscribeChain__record_music_download_facts( - subscribe=subscribe, - downloads=downloads, - ) + apply_update = self._SubscribeChain__apply_subscribe_update + update_note = self._SubscribeChain__update_subscribe_note + subscribe = _record_music_download_facts(apply_update, update_note, subscribe, downloads) elif downloads: subscribe = self._SubscribeChain__update_subscribe_note( subscribe=subscribe, @@ -252,54 +251,6 @@ class SubscribeCompletionOwner(_SubscribeOwnerBase): return updated or subscribe return subscribe - def _SubscribeChain__record_music_download_facts( - self, - subscribe: SubscriptionSnapshot, - downloads: Optional[List[Context]], - ) -> SubscriptionSnapshot: - """合并本轮专辑音轨事实,按统一媒体身份去重后返回最新订阅快照。""" - if not downloads: - return subscribe - if getattr(subscribe, "music_type", None) != MUSIC_ENTITY_ALBUM: - return self._SubscribeChain__update_subscribe_note( - subscribe=subscribe, - downloads=downloads, - ) - - downloaded_tracks = { - item - for item in (getattr(subscribe, "downloaded_tracks", None) or []) - if isinstance(item, str) and item - } - subscribe_identity = resolve_media_identity(media=subscribe) - for context in downloads: - media = context.media_info - if not media or media.type != MediaType.MUSIC: - continue - if subscribe_identity != resolve_media_identity(media=media): - continue - downloaded_tracks.update( - item - for item in (getattr(context, "music_track_keys", None) or []) - if isinstance(item, str) and item - ) - if not downloaded_tracks: - return subscribe - - updated = self._SubscribeChain__apply_subscribe_update( - subscribe, - { - "downloaded_tracks": sorted(downloaded_tracks), - "last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - }, - scene="music_download_tracks", - ) - logger.info( - f"订阅 {subscribe.name} 记录已下载专辑曲目:" - f"{len(downloaded_tracks)}/{subscribe.total_tracks or 0}" - ) - return updated or subscribe - @staticmethod def _SubscribeChain__get_downloaded(subscribe: SubscriptionSnapshot) -> List[int]: """ @@ -577,3 +528,50 @@ class SubscribeCompletionOwner(_SubscribeOwnerBase): ) logger.info(f"订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}") return False, no_exists + + +def _record_music_download_facts( + apply_update: Callable[..., SubscriptionSnapshot], + update_note: Callable[..., SubscriptionSnapshot], + subscribe: SubscriptionSnapshot, + downloads: Optional[List[Context]], +) -> SubscriptionSnapshot: + """合并本轮专辑音轨事实,按统一媒体身份去重后返回最新订阅快照。""" + if not downloads: + return subscribe + if getattr(subscribe, "music_type", None) != MUSIC_ENTITY_ALBUM: + return update_note(subscribe=subscribe, downloads=downloads) + + downloaded_tracks = { + item + for item in (getattr(subscribe, "downloaded_tracks", None) or []) + if isinstance(item, str) and item + } + subscribe_identity = resolve_media_identity(media=subscribe) + for context in downloads: + media = context.media_info + if not media or media.type != MediaType.MUSIC: + continue + if subscribe_identity != resolve_media_identity(media=media): + continue + downloaded_tracks.update( + item + for item in (getattr(context, "music_track_keys", None) or []) + if isinstance(item, str) and item + ) + if not downloaded_tracks: + return subscribe + + updated = apply_update( + subscribe, + { + "downloaded_tracks": sorted(downloaded_tracks), + "last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + }, + scene="music_download_tracks", + ) + logger.info( + f"订阅 {subscribe.name} 记录已下载专辑曲目:" + f"{len(downloaded_tracks)}/{subscribe.total_tracks or 0}" + ) + return updated or subscribe diff --git a/app/chain/subscribe/query.py b/app/chain/subscribe/query.py index 31345aeee..48c877f16 100644 --- a/app/chain/subscribe/query.py +++ b/app/chain/subscribe/query.py @@ -38,6 +38,14 @@ from app.schemas.types import ( ) +def _public_subscribe_view(subscribe: SubscriptionSnapshot) -> _SchemaSubscribe: + """将订阅内部快照投影为公开 DTO,并隐藏活动音轨事实。""" + payload = subscribe.to_dict() + payload["completed_tracks"] = compute_subscribe_completed_tracks(subscribe) + payload.pop("downloaded_tracks", None) + return _SchemaSubscribe(**payload) + + class SubscribeQueryOwner(_SubscribeOwnerBase): """订阅查询、关注与现存媒体投影,作为 SubscribeChain 的单一职责实现 owner。""" @@ -401,10 +409,7 @@ class SubscribeQueryOwner(_SubscribeOwnerBase): self._append_subscribe_media_servers(subscribe, mediainfo, episodes) # 更新订阅信息 - subscribe_payload = subscribe.to_dict() - subscribe_payload["completed_tracks"] = compute_subscribe_completed_tracks(subscribe) - subscribe_payload.pop("downloaded_tracks", None) - subscribe_info.subscribe = _SchemaSubscribe(**subscribe_payload) + subscribe_info.subscribe = _public_subscribe_view(subscribe) subscribe_info.episodes = episodes return subscribe_info