refactor(architecture): keep subscription owners within ratchet

This commit is contained in:
jxxghp
2026-09-14 19:58:27 +08:00
parent ebbc0d4520
commit e0adb47521
3 changed files with 85 additions and 79 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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