refactor(chain): 处理链功能域 mixin 化,清理未使用导入并根治兼容层循环导入

- ChainBase 拆分为 RecognitionMixin/MessageProcessingMixin/NotificationMixin
- TransferChain 拆分为 7 个功能 mixin(_mixins.py),SubscribeChain 音乐订阅域拆出 _music.py
- 斜杠命令交互四件套收敛为 InteractionChainMixin 委托,会话管理器移至 application 层,chain 层不再 re-export
- 模块基础类收敛到 app/modules/_base(notification/mediaserver 语义重命名)
- 清理 app/chain/__init__.py 24 个未使用导入,修正 49 处测试 patch 目标到实际命名空间
- 兼容层 legacy 符号不再并入 __all__,根治 schemas 初始化反向拉起 application.transfer 的循环导入
- 修复 bangumi 集数为字符串时 set_bangumi_info 抛 TypeError
- 新增重复代码等架构门禁测试;capability 清单校验排除下划线内部目录
This commit is contained in:
jxxghp
2026-08-16 16:30:16 +08:00
parent 24671f8f18
commit 7e851dbfa7
102 changed files with 6041 additions and 5888 deletions
+15 -161
View File
@@ -1,17 +1,16 @@
from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas
from app.domain.context import MediaInfo
from app.runtime.events import eventmanager
from app.application.mediaserver import MusicMediaServerHelper
from app.runtime.log import logger
from app.modules import _MediaServerBase, _ModuleBase
from app.modules._base import _MediaServerModuleBase
from app.modules.trimemedia.trimemedia import TrimeMedia
from app.schemas import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import ChainEventType, MediaServerType, MediaType, ModuleType
from app.schemas.types import MediaServerType, ModuleType
class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
class TrimeMediaModule(_MediaServerModuleBase[TrimeMedia]):
# 媒体库标识(ExistMediaInfo.server_type
_server_type_value = "trimemedia"
def init_module(self) -> None:
"""
@@ -52,15 +51,9 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
def scheduler_job(self) -> None:
"""
定时任务,每10分钟调用一次
"""
# 定时重连
for name, server in self.get_instances().items():
if server.is_configured() and server.is_inactive():
logger.info(f"飞牛影视 {name} 连接断开,尝试重连 ...")
server.reconnect()
def _is_inactive(self, server) -> bool:
"""未配置的实例不参与定时重连。"""
return server.is_configured() and server.is_inactive()
def stop(self) -> None:
"""停止模块"""
@@ -71,65 +64,12 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
except Exception as err:
logger.error(f"停止飞牛影视模块实例失败:{err}")
def test(self) -> Optional[Tuple[bool, str]]:
"""
测试模块连接性
"""
if not self.get_instances():
return None
for name, server in self.get_instances().items():
if not server.is_configured():
return False, f"飞牛影视配置不完整:{name}"
if server.is_inactive() and not server.reconnect():
return False, f"无法连接飞牛影视:{name}"
return True, ""
def user_authenticate(
self, credentials: AuthCredentials, service_name: Optional[str] = None
) -> Optional[AuthCredentials]:
"""
使用飞牛影视用户辅助完成用户认证
:param credentials: 认证数据
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
:return: 认证数据
"""
# 飞牛影视认证
if not credentials or credentials.grant_type != "password":
return None
# 确定要认证的服务器列表
if service_name:
# 如果指定了服务名,获取该服务实例
servers = (
[(service_name, server)]
if (server := self.get_instance(service_name))
else []
)
else:
# 如果没有指定服务名,遍历所有服务
servers = self.get_instances().items()
# 遍历要认证的服务器
for name, server in servers:
# 触发认证拦截事件
intercept_event = eventmanager.send_event(
etype=ChainEventType.AuthIntercept,
data=AuthInterceptCredentials(
username=credentials.username,
channel=self.get_name(),
service=name,
status="triggered",
),
)
if intercept_event and intercept_event.event_data:
intercept_data: AuthInterceptCredentials = intercept_event.event_data
if intercept_data.cancel:
continue
token = server.authenticate(credentials.username, credentials.password)
if token:
credentials.channel = self.get_name()
credentials.service = name
credentials.token = token
return credentials
def _test_server(self, server, name: str) -> Optional[str]:
"""飞牛影视用配置完整性与重连结果探测连接状态。"""
if not server.is_configured():
return f"{self.get_name()}配置不完整:{name}"
if server.is_inactive() and not server.reconnect():
return f"无法连接{self.get_name()}{name}"
return None
def webhook_parser(
@@ -160,92 +100,6 @@ class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
return result
return None
def media_exists(
self,
mediainfo: MediaInfo,
itemid: Optional[str] = None,
server: Optional[str] = None,
) -> Optional[schemas.ExistMediaInfo]:
"""
判断媒体文件是否存在
:param mediainfo: 识别的媒体信息
:param itemid: 媒体服务器ItemID
:param server: 媒体服务器名称
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
"""
if server:
servers = [(server, self.get_instance(server))]
else:
servers = self.get_instances().items()
for name, s in servers:
if not s:
continue
if mediainfo.type == MediaType.MUSIC:
matches = getattr(s, "get_music", lambda **_: [])(
**MusicMediaServerHelper.search_params(mediainfo)
)
match = MusicMediaServerHelper.find_match(mediainfo, matches)
if match:
return schemas.ExistMediaInfo(
type=MediaType.MUSIC,
server_type="trimemedia",
server=name,
itemid=match.item_id,
)
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:
movie = s.get_iteminfo(itemid)
if movie:
logger.info(f"媒体库 {name} 中找到了 {movie}")
return schemas.ExistMediaInfo(
type=MediaType.MOVIE,
server_type="trimemedia",
server=name,
itemid=movie.item_id,
)
movies = s.get_movies(
title=mediainfo.title,
year=mediainfo.year,
media_source=mediainfo.media_source,
media_id=mediainfo.media_id,
)
if not movies:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
else:
logger.info(f"媒体库 {name} 中找到了 {movies}")
return schemas.ExistMediaInfo(
type=MediaType.MOVIE,
server_type="trimemedia",
server=name,
itemid=movies[0].item_id,
)
else:
itemid, tvs = s.get_tv_episodes(
title=mediainfo.title,
year=mediainfo.year,
media_source=mediainfo.media_source,
media_id=mediainfo.media_id,
item_id=itemid,
)
if not tvs:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
else:
logger.info(
f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}"
)
return schemas.ExistMediaInfo(
type=MediaType.TV,
seasons=tvs,
server_type="trimemedia",
server=name,
itemid=itemid,
)
return None
def media_statistic(
self, server: Optional[str] = None
) -> Optional[List[schemas.Statistic]]: