mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor(chain): chain 与 module 仅经 run_module 契约互联,实现模块内容封闭
- mTorrent 字幕链接解析下沉为 IndexerModule.site_subtitle_links,subtitle 模块自行跳过 API 站点 - TMDB/MusicBrainz 识别缓存管理改为模块方法(tmdb_cache_*/music_cache_*)经 run_module 分发 - WechatClawBot 客户端查找/临时客户端/缓存迁移内聚为 wechatclawbot_* 模块方法, chain 移除 WechatClawBot 类与 ModuleManager 内省 - LISTENBRAINZ_* 常量迁至 schemas/types.py,模块与链层再导入保持兼容 - TMDbException 升为 schemas/exception.py 跨层契约,vendored 异常保持类身份一致 - 架构守护测试新增 test_chain_does_not_import_module_internals(共 18 项) - 文档同步:chain->module 仅允许 run_module 分发,直接导入禁止
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from app.domain.context import SubtitleInfo, TorrentInfo
|
||||
from app.domain.context import Context, SubtitleInfo, TorrentInfo
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.foundation.reflection import ModuleHelper
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
@@ -94,6 +94,28 @@ class IndexerModule(_ModuleBase):
|
||||
"""索引模块无需独立开关配置"""
|
||||
pass
|
||||
|
||||
def site_subtitle_links(self, context: Context) -> Optional[List[str]]:
|
||||
"""
|
||||
解析采用API访问的站点的字幕下载链接,非API站点返回None交由页面解析模块处理
|
||||
:param context: 上下文,包括识别信息、媒体信息、种子信息
|
||||
:return: 字幕下载链接列表,不适用时返回None
|
||||
"""
|
||||
torrent = context.torrent_info
|
||||
if torrent.site is None:
|
||||
return None
|
||||
site = SiteOper().get(torrent.site)
|
||||
if not site:
|
||||
return None
|
||||
indexer = SitesHelper().get_indexer(site.domain)
|
||||
if not indexer:
|
||||
return None
|
||||
if indexer.get("parser") == "mTorrent":
|
||||
return MTorrentSpider(indexer).get_subtitle_links(
|
||||
torrent.page_url
|
||||
)
|
||||
# TODO 其它采用API访问的站点
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __search_check(site: dict, search_word: Optional[str] = None) -> bool:
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,9 @@ from app.domain.context import MusicInfo
|
||||
from app.runtime.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.schemas.types import (
|
||||
LISTENBRAINZ_CHART_RANGES,
|
||||
LISTENBRAINZ_FRESH_MAX_DAYS,
|
||||
LISTENBRAINZ_FRESH_SORTS,
|
||||
MUSIC_ENTITY_ALBUM,
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
MediaSource,
|
||||
@@ -14,27 +17,6 @@ from app.schemas.types import (
|
||||
)
|
||||
from app.adapters.network.http import RequestUtils
|
||||
|
||||
# ListenBrainz 全站统计支持的周期,取值与官方统计页面完全一致
|
||||
LISTENBRAINZ_CHART_RANGES = (
|
||||
"this_week",
|
||||
"this_month",
|
||||
"this_year",
|
||||
"week",
|
||||
"month",
|
||||
"quarter",
|
||||
"half_yearly",
|
||||
"year",
|
||||
"all_time",
|
||||
)
|
||||
# ListenBrainz 官方新发行页面支持的排序方式
|
||||
LISTENBRAINZ_FRESH_SORTS = (
|
||||
"release_date",
|
||||
"artist_credit_name",
|
||||
"release_name",
|
||||
)
|
||||
# ListenBrainz 新发行页面允许回溯或预告的最大天数
|
||||
LISTENBRAINZ_FRESH_MAX_DAYS = 90
|
||||
|
||||
|
||||
class ListenBrainzModule(_ModuleBase):
|
||||
"""通过 ListenBrainz 全站统计与新发行数据提供音乐探索能力。"""
|
||||
|
||||
@@ -111,6 +111,19 @@ class MusicBrainzModule(_ModuleBase):
|
||||
self.cache.clear()
|
||||
logger.info("音乐识别缓存清除完成")
|
||||
|
||||
def music_cache_items(self) -> list[dict]:
|
||||
"""查询音乐识别缓存条目列表。"""
|
||||
return self.cache.list_items() if self.cache else []
|
||||
|
||||
def music_cache_delete(self, cache_key: str) -> dict:
|
||||
"""按缓存键删除单条音乐识别缓存。"""
|
||||
return self.cache.delete(cache_key) if self.cache else {}
|
||||
|
||||
def music_cache_clear(self) -> None:
|
||||
"""清空全部音乐识别缓存。"""
|
||||
if self.cache:
|
||||
self.cache.clear()
|
||||
|
||||
def test(self) -> Tuple[bool, str]:
|
||||
"""测试 MusicBrainz 搜索接口连通性。"""
|
||||
result = self._request_json(
|
||||
|
||||
@@ -6,6 +6,8 @@ from lxml import etree
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import Context
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.runtime.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.schemas.types import ModuleType, OtherModulesType
|
||||
@@ -133,6 +135,12 @@ class SubtitleModule(_ModuleBase):
|
||||
torrent = context.torrent_info
|
||||
if not torrent.page_url:
|
||||
return None
|
||||
# 采用API访问的站点由对应爬虫模块处理,详情页HTML不含字幕元素
|
||||
if torrent.site is not None:
|
||||
site = SiteOper().get(torrent.site)
|
||||
if site and (indexer := SitesHelper().get_indexer(site.domain)):
|
||||
if indexer.get("parser") == "mTorrent":
|
||||
return None
|
||||
request = RequestUtils(
|
||||
cookies=torrent.site_cookie,
|
||||
ua=torrent.site_ua,
|
||||
|
||||
@@ -818,6 +818,24 @@ class TheMovieDbModule(_ModuleBase):
|
||||
"""
|
||||
return self.update_recognize_cache(meta=meta, mediainfo=mediainfo)
|
||||
|
||||
def tmdb_cache_items(self) -> list:
|
||||
"""
|
||||
查询TMDB识别缓存条目列表
|
||||
"""
|
||||
return self.cache.list_items()
|
||||
|
||||
def tmdb_cache_delete(self, cache_key: str) -> dict:
|
||||
"""
|
||||
按缓存键删除单条TMDB识别缓存
|
||||
"""
|
||||
return self.cache.delete(cache_key)
|
||||
|
||||
def tmdb_cache_clear(self) -> None:
|
||||
"""
|
||||
清空全部TMDB识别缓存
|
||||
"""
|
||||
self.cache.clear()
|
||||
|
||||
def media_category(self) -> Optional[Dict[str, list]]:
|
||||
"""
|
||||
获取媒体分类
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class TMDbException(Exception):
|
||||
pass
|
||||
# TMDbException 是跨层异常契约,定义在 schemas 层,供链层与API层捕获;
|
||||
# 模块内部沿用原导入路径,保持类身份一致
|
||||
from app.schemas.exception import TMDbException # noqa: F401
|
||||
|
||||
|
||||
class TMDbConnectionError(TMDbException):
|
||||
|
||||
@@ -83,6 +83,65 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
|
||||
"""初始化模块设置。"""
|
||||
pass
|
||||
|
||||
def wechatclawbot_client(
|
||||
self,
|
||||
source: Optional[str] = None,
|
||||
fallback_source: Optional[str] = None,
|
||||
):
|
||||
"""按名称解析已加载的微信 ClawBot 客户端,候选名均无配置时返回默认实例。"""
|
||||
source_name = str(source or "").strip() or None
|
||||
fallback_name = str(fallback_source or "").strip() or None
|
||||
candidate_names = []
|
||||
for candidate in (fallback_name, source_name):
|
||||
if candidate and candidate not in candidate_names:
|
||||
candidate_names.append(candidate)
|
||||
if candidate_names:
|
||||
for candidate in candidate_names:
|
||||
config = self.get_config(candidate)
|
||||
if not config:
|
||||
continue
|
||||
client = self.get_instance(config.name)
|
||||
if client:
|
||||
return client
|
||||
return None
|
||||
return self.get_instance()
|
||||
|
||||
def wechatclawbot_temp_client(
|
||||
self,
|
||||
source: Optional[str] = None,
|
||||
WECHATCLAWBOT_BASE_URL: Optional[str] = None,
|
||||
WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None,
|
||||
WECHATCLAWBOT_ADMINS: Optional[str] = None,
|
||||
WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None,
|
||||
):
|
||||
"""基于当前表单配置创建一个临时客户端,用于未保存时的扫码状态预览。"""
|
||||
source_name = str(source or "").strip()
|
||||
if not source_name:
|
||||
return None
|
||||
return WechatClawBot(
|
||||
name=source_name,
|
||||
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
|
||||
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
|
||||
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
|
||||
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
|
||||
auto_start_polling=False,
|
||||
)
|
||||
|
||||
def wechatclawbot_migrate_cache(
|
||||
self,
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
cleanup_old: bool = False,
|
||||
overwrite: bool = False,
|
||||
):
|
||||
"""在通知名称变更时迁移对应的微信 ClawBot 登录缓存。"""
|
||||
return WechatClawBot.migrate_cached_state(
|
||||
old_name=old_name,
|
||||
new_name=new_name,
|
||||
cleanup_old=cleanup_old,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _load_json(body: Any) -> Optional[dict]:
|
||||
"""将内容解析为 JSON 字典。"""
|
||||
|
||||
Reference in New Issue
Block a user