refactor: complete runtime configuration migration

This commit is contained in:
jxxghp
2026-08-22 19:07:07 +08:00
parent d131f8d571
commit 1bb6e36e8c
80 changed files with 1432 additions and 581 deletions
+17
View File
@@ -9,6 +9,10 @@ from typing import Optional, Any, Tuple, List, Set, Union, Dict
from app.application.chain.context import ChainRuntimeContext, get_chain_runtime_context
from app.application.chain.data import get_chain_data_ports
from app.application.configuration import (
ChainRuntimeConfig,
get_chain_runtime_config_snapshot,
)
from app.chain._messaging import MessageProcessingMixin, NotificationMixin
from app.chain._recognition import RecognitionMixin
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
@@ -64,6 +68,19 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
)
self.messagequeue = context.message_queue_factory(self.run_module)
@property
def runtime_config(self) -> ChainRuntimeConfig:
"""返回实例快照;兼容绕过构造器的旧调用并按需取得当前快照。"""
configuration = getattr(self, "_runtime_config", None)
if configuration is None:
return get_chain_runtime_config_snapshot()
return configuration
@runtime_config.setter
def runtime_config(self, configuration: ChainRuntimeConfig) -> None:
"""保存显式注入的 Chain 配置快照。"""
self._runtime_config = configuration
def load_cache(self, filename: str) -> Any:
"""
加载缓存
+13 -10
View File
@@ -26,13 +26,16 @@ from app.application.chain.data import (
DownloadHistoryPortProxy as DownloadHistoryOper,
TransferHistoryPortProxy as TransferHistoryOper,
)
from app.application.configuration import get_configured_system_config
from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.domain.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.foundation import text as text_tools
from app.runtime.config import global_vars, settings
from app.runtime.config import global_vars
from app.runtime.log import logger
from app.schemas.workflow import FileItem
from app.schemas.message import Message
@@ -320,7 +323,7 @@ class FileFilterMixin:
"""
if history.type == MediaType.MUSIC.value:
return True
return src_path.suffix.lower() in settings.RMT_AUDIOEXT
return src_path.suffix.lower() in get_chain_runtime_config_snapshot().audio_extensions
def _recognize_music_retry_media(
self,
@@ -1338,7 +1341,7 @@ class FailedRetryMixin:
userid=userid,
username=username,
title=f"整理记录 #{history_id} 已重新整理",
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -1352,7 +1355,7 @@ class FailedRetryMixin:
username=username,
title="重新整理失败",
text=errmsg,
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -1369,7 +1372,7 @@ class FailedRetryMixin:
由智能助手接管一条失败的整理记录。
"""
if not settings.AI_AGENT_ENABLE:
if not self.runtime_config.ai_agent_enable:
self.post_message(
Message(
channel=channel,
@@ -1392,7 +1395,7 @@ class FailedRetryMixin:
username=username,
title="重新整理失败",
text=f"整理记录 #{history_id} 不存在",
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -1408,7 +1411,7 @@ class FailedRetryMixin:
username=username,
title=f"已将整理记录 #{history_id} 交给智能助手处理",
text="处理完成后会在这里回复结果。",
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -1440,7 +1443,7 @@ class FailedRetryMixin:
title="智能助手整理完成",
text=final_output.strip()
or f"整理记录 #{history_id} 已由智能助手处理完成。",
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -1453,7 +1456,7 @@ class FailedRetryMixin:
username=username,
title="智能助手整理失败",
text=str(e),
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
+36 -22
View File
@@ -16,7 +16,8 @@ from app.chain import ChainBase
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.runtime.cache import FileCache
from app.runtime.config import settings, global_vars
from app.runtime.config import global_vars
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain.context import (
Context,
MediaInfo,
@@ -128,7 +129,7 @@ class DownloadChain(ChainBase):
mtype=MessageType.Download,
ctype=ContentType.DownloadAdded,
image=media.get_message_image(),
link=settings.MP_DOMAIN('/#/downloading'),
link=self.runtime_config.downloading_url,
userid=userid,
username=username,
),
@@ -171,7 +172,8 @@ class DownloadChain(ChainBase):
track_identities = {
identity
for file in file_list
if Path(str(file)).suffix.lower() in settings.RMT_AUDIOEXT
if Path(str(file)).suffix.lower()
in get_chain_runtime_config_snapshot().audio_extensions
and (identity := DownloadChain._music_resource_track_identity(file))
}
actual_tracks = len(track_identities)
@@ -264,7 +266,10 @@ class DownloadChain(ChainBase):
"""
判断是否为支持的字幕文件。
"""
return Path(file_name).suffix.lower() in settings.RMT_SUBEXT
return (
Path(file_name).suffix.lower()
in get_chain_runtime_config_snapshot().subtitle_extensions
)
@classmethod
def _get_subtitle_working_dir(
@@ -441,10 +446,10 @@ class DownloadChain(ChainBase):
return False, message, []
saved_files = []
temp_file = settings.TEMP_PATH / file_name
temp_file = self.runtime_config.temporary_path / file_name
temp_extract_dir = temp_file.with_name(temp_file.stem)
try:
settings.TEMP_PATH.mkdir(parents=True, exist_ok=True)
self.runtime_config.temporary_path.mkdir(parents=True, exist_ok=True)
temp_file.write_bytes(response.content)
if self._is_subtitle_archive(file_name):
try:
@@ -457,7 +462,10 @@ class DownloadChain(ChainBase):
message = f"字幕压缩包解压失败:{str(err)}"
logger.error(f"{message},文件:{temp_file}")
return False, message, []
for sub_file in SystemUtils.list_files(temp_extract_dir, settings.RMT_SUBEXT):
for sub_file in SystemUtils.list_files(
temp_extract_dir,
self.runtime_config.subtitle_extensions,
):
uploaded_path, message = self._upload_subtitle_file(
storage_chain=storage_chain,
storage=storage,
@@ -537,8 +545,8 @@ class DownloadChain(ChainBase):
request = RequestUtils(
cookies=subtitle.site_cookie,
ua=subtitle.site_ua or settings.USER_AGENT,
proxies=settings.PROXY if subtitle.site_proxy else None,
ua=subtitle.site_ua or self.runtime_config.user_agent,
proxies=self.runtime_config.proxy if subtitle.site_proxy else None,
)
try:
response = request.get_res(subtitle.enclosure, raise_exception=True)
@@ -621,7 +629,7 @@ class DownloadChain(ChainBase):
:param download_dir: 下载目录
:param torrent_content: 种子内容,如果是种子文件,则为文件内容,否则为种子字符串
"""
if not settings.DOWNLOAD_SUBTITLE:
if not self.runtime_config.download_subtitle:
return
# 没有种子文件不处理
@@ -673,9 +681,9 @@ class DownloadChain(ChainBase):
request = RequestUtils(
cookies=torrent.site_cookie,
ua=torrent.site_ua,
proxies=settings.PROXY if torrent.site_proxy else None,
proxies=self.runtime_config.proxy if torrent.site_proxy else None,
)
settings.TEMP_PATH.mkdir(parents=True, exist_ok=True)
self.runtime_config.temporary_path.mkdir(parents=True, exist_ok=True)
for sublink in sublink_list:
logger.info(f"找到字幕下载链接:{sublink},开始下载...")
# 下载
@@ -687,7 +695,7 @@ class DownloadChain(ChainBase):
continue
archive_format = self._SUBTITLE_ARCHIVE_FORMATS.get(Path(file_name).suffix.lower())
if archive_format:
archive_file = settings.TEMP_PATH / file_name
archive_file = self.runtime_config.temporary_path / file_name
# 保存
archive_file.write_bytes(ret.content)
# 解压路径
@@ -700,7 +708,10 @@ class DownloadChain(ChainBase):
archive_format=archive_format,
)
# 遍历转移文件
for sub_file in SystemUtils.list_files(archive_path, settings.RMT_SUBEXT):
for sub_file in SystemUtils.list_files(
archive_path,
self.runtime_config.subtitle_extensions,
):
target_sub_file = Path(working_dir_item.path) / Path(sub_file.name)
if storage_chain.get_file_item(storage, target_sub_file):
logger.info(f"字幕文件已存在:{target_sub_file}")
@@ -718,10 +729,13 @@ class DownloadChain(ChainBase):
except Exception as err:
logger.error(f"删除临时文件失败:{str(err)}")
else:
if Path(file_name).suffix.lower() not in settings.RMT_SUBEXT:
if (
Path(file_name).suffix.lower()
not in self.runtime_config.subtitle_extensions
):
logger.warn(f"链接不是支持的字幕文件:{sublink} - {file_name}")
continue
sub_file = settings.TEMP_PATH / file_name
sub_file = self.runtime_config.temporary_path / file_name
# 保存
sub_file.write_bytes(ret.content)
target_sub_file = Path(working_dir_item.path) / Path(sub_file.name)
@@ -953,7 +967,7 @@ class DownloadChain(ChainBase):
ua=ua,
cookies=cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
proxies=get_chain_runtime_config_snapshot().proxy if proxy else None
).get_res(url, params=req_params.get('params'))
else:
# POST请求
@@ -961,7 +975,7 @@ class DownloadChain(ChainBase):
ua=ua,
cookies=cookie,
headers=headers,
proxies=settings.PROXY if proxy else None
proxies=get_chain_runtime_config_snapshot().proxy if proxy else None
).post_res(url, params=req_params.get('params'))
if not res:
return None
@@ -1016,7 +1030,7 @@ class DownloadChain(ChainBase):
_, content, download_folder, files, error_msg = TorrentHelper().download_torrent(
url=torrent_url,
cookie=site_cookie,
ua=torrent.site_ua or settings.USER_AGENT,
ua=torrent.site_ua or self.runtime_config.user_agent,
proxy=torrent.site_proxy,
cache_invalid=not indirect_download)
@@ -1255,7 +1269,7 @@ class DownloadChain(ChainBase):
or file_meta.begin_episode not in episodes:
continue
# 只处理音视频、字幕格式
media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIOEXT
media_exts = self.runtime_config.media_extensions
if not Path(file).suffix \
or Path(file).suffix.lower() not in media_exts:
continue
@@ -2074,7 +2088,7 @@ class DownloadChain(ChainBase):
mtype=MessageType.Download,
title="没有正在下载的任务!",
userid=userid,
link=settings.MP_DOMAIN('#/downloading'),
link=self.runtime_config.downloading_url,
save_history=False,
))
return
@@ -2094,7 +2108,7 @@ class DownloadChain(ChainBase):
title=title,
text="\n".join(messages),
userid=userid,
link=settings.MP_DOMAIN('#/downloading'),
link=self.runtime_config.downloading_url,
save_history=False,
))
+6 -6
View File
@@ -12,7 +12,7 @@ from app.chain.douban import DoubanChain
from app.chain.musicbrainz import MusicBrainzChain, _MusicMetadataSourceChain
from app.chain.theaudiodb import TheAudioDbChain
from app.runtime.cache import async_fresh, fresh
from app.runtime.config import settings
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain.context import (
Context,
MediaInfo,
@@ -172,7 +172,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
@classmethod
def _simplify_recognized_music_info(cls, info: MusicInfo) -> MusicInfo:
"""按开关转换标准音乐文本字段,并避免修改来源模块的缓存对象。"""
if not settings.MUSIC_METADATA_TO_SIMPLIFIED:
if not get_chain_runtime_config_snapshot().music_metadata_to_simplified:
return info
updates: dict[str, Any] = {}
for field_name in cls._music_simplified_text_fields:
@@ -462,7 +462,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
is_recognized = lambda result: bool(result)
mediainfo = None
plugin_available = eventmanager.check(plugin_event)
if settings.RECOGNIZE_PLUGIN_FIRST and plugin_available:
if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available:
# 插件优先
logger.info(f"插件识别优先模式已开启。请求辅助识别,标题:{log_name} ...")
helped = plugin_fn()
@@ -912,7 +912,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
@classmethod
def is_audio_path(cls, path: Union[str, Path]) -> bool:
"""判断路径是否指向系统支持的音频文件。"""
return Path(path).suffix.lower() in settings.RMT_AUDIOEXT
return Path(path).suffix.lower() in get_chain_runtime_config_snapshot().audio_extensions
@classmethod
def read_path_meta(cls, path: Union[str, Path]) -> MetaMusic:
@@ -1094,7 +1094,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
item for item in entries
if not item.name.startswith(".")
and item.is_file()
and item.suffix.lower() in settings.RMT_AUDIOEXT
and item.suffix.lower() in get_chain_runtime_config_snapshot().audio_extensions
)
collect(directory)
@@ -1525,7 +1525,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
is_recognized = lambda result: bool(result)
mediainfo = None
plugin_available = eventmanager.check(plugin_event)
if settings.RECOGNIZE_PLUGIN_FIRST and plugin_available:
if get_chain_runtime_config_snapshot().recognize_plugin_first and plugin_available:
# 插件优先
logger.info(f"插件优先模式已开启。请求辅助识别,标题:{log_name} ...")
helped = await plugin_fn()
+19 -9
View File
@@ -21,7 +21,7 @@ from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain
from app.chain.interaction import MediaInteractionChain as _MediaInteractionChain
from app.runtime.config import settings, global_vars
from app.runtime.config import global_vars
from app.application.messaging.agent import agent_interaction_manager, parse_agent_choice_callback
from app.application.messaging.interaction import InteractionContext, InteractionDispatch
from app.application.messaging.media import media_interaction_manager
@@ -477,8 +477,13 @@ class MessageChain(ChainBase):
if (
not no_ai_requested
and
settings.AI_AGENT_ENABLE
and (settings.AI_AGENT_GLOBAL or images or files or has_audio_input)
self.runtime_config.ai_agent_enable
and (
self.runtime_config.ai_agent_global
or images
or files
or has_audio_input
)
):
return self._handle_ai_message(
text=text,
@@ -554,8 +559,13 @@ class MessageChain(ChainBase):
if text.startswith("/"):
return False
if not (
settings.AI_AGENT_ENABLE
and (settings.AI_AGENT_GLOBAL or images or files or has_audio_input)
self.runtime_config.ai_agent_enable
and (
self.runtime_config.ai_agent_global
or images
or files
or has_audio_input
)
):
return False
if self._interaction_router().has_pending(userid):
@@ -1223,7 +1233,7 @@ class MessageChain(ChainBase):
"""
try:
# 检查AI智能体是否启用
if not settings.AI_AGENT_ENABLE:
if not self.runtime_config.ai_agent_enable:
self.post_message(
Message(
channel=channel,
@@ -1280,8 +1290,8 @@ class MessageChain(ChainBase):
original_images = images
all_files = list(files or [])
if images and supports_image_input(
provider=settings.LLM_PROVIDER,
model=settings.LLM_MODEL,
provider=self.runtime_config.llm_provider,
model=self.runtime_config.llm_model,
):
images = self._download_attachments_to_data_urls(
images, channel, source
@@ -1829,7 +1839,7 @@ class MessageChain(ChainBase):
将用户上传文件写入临时目录,并返回本地路径。
"""
safe_name = self._sanitize_attachment_name(filename, mime_type)
base_dir = settings.TEMP_PATH / "agent_uploads" / session_id
base_dir = self.runtime_config.temporary_path / "agent_uploads" / session_id
base_dir.mkdir(parents=True, exist_ok=True)
file_id = uuid.uuid4().hex[:8]
+19 -11
View File
@@ -12,7 +12,6 @@ from app.chain import ChainBase
from app.chain.lrclib import LrclibChain
from app.chain.storage import StorageChain
from app.runtime.cache import cached
from app.runtime.config import settings
from app.domain.context import (
MediaInfo,
MusicAlbumInfo,
@@ -23,7 +22,10 @@ from app.runtime.events import eventmanager, Event
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.application.configuration import get_configured_system_config
from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.audio import AudioMetadataHelper
from app.runtime.log import logger
from app.schemas.workflow import FileItem
@@ -314,7 +316,8 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
try:
logger.info(f"正在下载图片:{url} ...")
request_utils = RequestUtils(
proxies=settings.PROXY, ua=settings.NORMAL_USER_AGENT
proxies=self.runtime_config.proxy,
ua=self.runtime_config.normal_user_agent,
)
with request_utils.get_stream(url=url) as r:
if r and r.status_code == 200:
@@ -932,7 +935,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
or isinstance(meta, MetaMusic)
or (
fileitem.type == "file"
and filepath.suffix.lower() in settings.RMT_AUDIOEXT
and filepath.suffix.lower() in self.runtime_config.audio_extensions
)
)
if is_music:
@@ -953,7 +956,8 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
**music_kwargs,
)
if fileitem.type == "file" and (
not filepath.suffix or filepath.suffix.lower() not in settings.RMT_MEDIAEXT
not filepath.suffix
or filepath.suffix.lower() not in self.runtime_config.video_extensions
):
return False, "刮削路径不是支持的媒体文件"
@@ -1130,12 +1134,16 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
)
@staticmethod
@cached(maxsize=64, ttl=settings.CONF.meta, skip_none=True)
@cached(
maxsize=64,
ttl_provider=lambda: get_chain_runtime_config_snapshot().metadata_cache_ttl,
skip_none=True,
)
def _request_music_cover(url: str) -> Optional[tuple[Optional[bytes], str]]:
"""下载并缓存音乐封面;仅稳定 404 与成功响应进入有界缓存。"""
response = RequestUtils(
proxies=settings.PROXY,
ua=settings.NORMAL_USER_AGENT,
proxies=get_chain_runtime_config_snapshot().proxy,
ua=get_chain_runtime_config_snapshot().normal_user_agent,
timeout=20,
).get_res(url)
if response is None:
@@ -1161,7 +1169,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
@staticmethod
def _is_music_audio_file(path: str) -> bool:
"""判断路径是否指向系统支持的音频文件。"""
return Path(path).suffix.lower() in settings.RMT_AUDIOEXT
return Path(path).suffix.lower() in get_chain_runtime_config_snapshot().audio_extensions
def _music_audio_fileitems(self, fileitem: _SchemaFileItem) -> list[_SchemaFileItem]:
"""展开待刮削目录并过滤系统支持的音频文件。"""
@@ -1803,7 +1811,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
for file in files:
if (
file.type == "dir"
and file.name not in settings.RENAME_FORMAT_S0_NAMES
and file.name not in self.runtime_config.season_zero_names
and MetaInfo(file.name).begin_season is None
):
# 电视剧不处理非季子目录
@@ -1843,7 +1851,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
season_meta = MetaInfo(filepath.name)
# 特殊季目录处理(Specials/SPs
if filepath.name in settings.RENAME_FORMAT_S0_NAMES:
if filepath.name in self.runtime_config.season_zero_names:
season_meta.begin_season = 0
elif season_meta.name and season_meta.begin_season is not None:
# 排除辅助词重新识别,避免误判根目录 (issue https://github.com/jxxghp/MoviePilot/issues/5501)
+43 -22
View File
@@ -14,14 +14,17 @@ from fastapi.concurrency import run_in_threadpool
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.runtime.config import global_vars, settings
from app.runtime.config import global_vars
from app.domain.context import Context
from app.domain.context import MediaInfo, SubtitleInfo, TorrentInfo
from app.runtime.events import eventmanager, Event
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.domain.context import MusicInfo
from app.application.configuration import get_configured_system_config
from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.search.state import (
@@ -155,7 +158,7 @@ class SearchChain(ChainBase):
settings 可能被环境变量写成字符串这里统一兜底为 1避免异常配置导致搜索中断
"""
pages = settings.SEARCH_RESOURCE_PAGES
pages = get_chain_runtime_config_snapshot().search_resource_pages
try:
pages = int(pages)
except (TypeError, ValueError):
@@ -199,7 +202,10 @@ class SearchChain(ChainBase):
"""
检查AI推荐功能是否已启用
"""
return settings.AI_AGENT_ENABLE and settings.AI_RECOMMEND_ENABLED
return (
self.runtime_config.ai_agent_enable
and self.runtime_config.ai_recommend_enabled
)
@staticmethod
def _calculate_recommend_request_hash(
@@ -425,7 +431,7 @@ class SearchChain(ChainBase):
"""
items: List[str] = []
valid_indices: List[int] = []
max_items = settings.AI_RECOMMEND_MAX_ITEMS or 50
max_items = get_chain_runtime_config_snapshot().ai_recommend_max_items or 50
if filtered_indices:
results_to_process = [
@@ -555,7 +561,7 @@ class SearchChain(ChainBase):
return
user_preference = (
settings.AI_RECOMMEND_USER_PREFERENCE
self.runtime_config.ai_recommend_user_preference
or "Prefer high-quality resources with more seeders"
)
search_results_text = (
@@ -1211,8 +1217,9 @@ class SearchChain(ChainBase):
mediainfo.tw_title,
mediainfo.sg_title] if k]))
# 限制搜索关键词数量
if settings.MAX_SEARCH_NAME_LIMIT:
keywords = keywords[:settings.MAX_SEARCH_NAME_LIMIT]
max_names = get_chain_runtime_config_snapshot().max_search_name_limit
if max_names:
keywords = keywords[:max_names]
return season_episodes, keywords
@@ -1274,7 +1281,10 @@ class SearchChain(ChainBase):
finished_count = 0
filtered_by_site: Dict[Tuple[Optional[int], Optional[str]], List[TorrentInfo]] = {}
max_workers = min(len(site_torrents), settings.CONF.threadpool or len(site_torrents))
max_workers = min(
len(site_torrents),
self.runtime_config.search_threadpool_size or len(site_torrents),
)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
all_tasks = {
executor.submit(__do_site_filter, site_torrent_list): site_key
@@ -1539,7 +1549,7 @@ class SearchChain(ChainBase):
mediainfo,
)
torrents.extend(matched_torrents)
if matched_torrents and not settings.SEARCH_MULTIPLE_NAME:
if matched_torrents and not self.runtime_config.search_multiple_name:
break
return self._build_music_contexts(
torrents=torrents,
@@ -1572,7 +1582,7 @@ class SearchChain(ChainBase):
mediainfo,
)
torrents.extend(matched_torrents)
if matched_torrents and not settings.SEARCH_MULTIPLE_NAME:
if matched_torrents and not self.runtime_config.search_multiple_name:
break
return await run_in_threadpool(
self._build_music_contexts,
@@ -1618,7 +1628,7 @@ class SearchChain(ChainBase):
"items": [],
"total_items": len(torrents),
}
if keyword_matched and not settings.SEARCH_MULTIPLE_NAME:
if keyword_matched and not self.runtime_config.search_multiple_name:
break
contexts = await run_in_threadpool(
@@ -1726,7 +1736,7 @@ class SearchChain(ChainBase):
torrents.extend(results)
# 有结果则停止
if not settings.SEARCH_MULTIPLE_NAME and torrents:
if not self.runtime_config.search_multiple_name and torrents:
logger.info(f"共搜索到 {len(torrents)} 个资源,停止搜索")
break
@@ -1816,7 +1826,7 @@ class SearchChain(ChainBase):
)
search_count += 1
# 未开启多名称搜索时,有结果则停止
if not settings.SEARCH_MULTIPLE_NAME and torrents:
if not self.runtime_config.search_multiple_name and torrents:
logger.info(f"共搜索到 {len(torrents)} 个资源,停止搜索")
break
@@ -1918,7 +1928,7 @@ class SearchChain(ChainBase):
}
search_count += 1
if not settings.SEARCH_MULTIPLE_NAME and torrents:
if not self.runtime_config.search_multiple_name and torrents:
logger.info(f"共搜索到 {len(torrents)} 个资源,停止搜索")
break
@@ -2156,7 +2166,7 @@ class SearchChain(ChainBase):
) or []
)
search_count += 1
if not settings.SEARCH_MULTIPLE_NAME and subtitles:
if not self.runtime_config.search_multiple_name and subtitles:
logger.info(f"共搜索到 {len(subtitles)} 个字幕,停止搜索")
break
@@ -2246,7 +2256,7 @@ class SearchChain(ChainBase):
}
search_count += 1
if not settings.SEARCH_MULTIPLE_NAME and subtitles:
if not self.runtime_config.search_multiple_name and subtitles:
logger.info(f"共搜索到 {len(subtitles)} 个字幕,停止搜索")
break
@@ -2331,7 +2341,10 @@ class SearchChain(ChainBase):
# 结果集
results = []
# 同一站点按页顺序抓取,避免空页后仍继续请求该站点的后续页。
max_workers = min(len(indexer_sites), settings.CONF.threadpool or len(indexer_sites))
max_workers = min(
len(indexer_sites),
self.runtime_config.search_threadpool_size or len(indexer_sites),
)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
pending_tasks = {}
@@ -2444,7 +2457,9 @@ class SearchChain(ChainBase):
text=f"开始搜索,共 {len(indexer_sites)} 个站点,{len(search_pages)} 页 ...")
# 结果集
results = []
semaphore = asyncio.Semaphore(settings.CONF.threadpool or total_num)
semaphore = asyncio.Semaphore(
self.runtime_config.search_threadpool_size or total_num
)
async def search_site_page(site: dict, search_page: int) -> List[TorrentInfo]:
"""
@@ -2579,7 +2594,9 @@ class SearchChain(ChainBase):
"total": total_num
}
semaphore = asyncio.Semaphore(settings.CONF.threadpool or total_num)
semaphore = asyncio.Semaphore(
self.runtime_config.search_threadpool_size or total_num
)
async def search_site(site: dict, search_page: int) -> List[TorrentInfo]:
"""
@@ -2701,7 +2718,9 @@ class SearchChain(ChainBase):
await progress.update(value=0,
text=f"开始搜索字幕,共 {len(indexer_sites)} 个站点,{len(search_pages)} 页 ...")
results = []
semaphore = asyncio.Semaphore(settings.CONF.threadpool or total_num)
semaphore = asyncio.Semaphore(
self.runtime_config.search_threadpool_size or total_num
)
async def search_site_page(site: dict, search_page: int) -> List[SubtitleInfo]:
"""
@@ -2816,7 +2835,9 @@ class SearchChain(ChainBase):
"total": total_num
}
semaphore = asyncio.Semaphore(settings.CONF.threadpool or total_num)
semaphore = asyncio.Semaphore(
self.runtime_config.search_threadpool_size or total_num
)
async def search_site(site: dict, search_page: int) -> List[SubtitleInfo]:
"""
+17 -14
View File
@@ -23,7 +23,7 @@ from app.chain.mediaserver import MediaServerChain
from app.chain.search import SearchChain
from app.chain.tmdb import TmdbChain
from app.chain.torrents import TorrentsChain
from app.runtime.config import settings, global_vars
from app.runtime.config import global_vars
from app.domain.context import (
Context,
MediaInfo,
@@ -39,7 +39,10 @@ from app.application.chain.data import (
SitePortProxy as SiteOper,
SubscribePortProxy as SubscribeOper,
)
from app.application.configuration import get_configured_system_config
from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.messaging.subscribe import SubscribeInteractionHandler
from app.application.mediaserver import MediaServerHelper
from app.application.subscription.write import add_subscribe, async_add_subscribe
@@ -874,10 +877,10 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
def __subscribe_added_link(mtype: MediaType) -> str:
"""返回订阅类型对应的前端详情入口。"""
if mtype == MediaType.TV:
return settings.MP_DOMAIN('#/subscribe/tv?tab=mysub')
return get_chain_runtime_config_snapshot().television_subscribe_url
if mtype == MediaType.MUSIC:
return settings.MP_DOMAIN('#/subscribe/music?tab=mysub')
return settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
return get_chain_runtime_config_snapshot().music_subscribe_url
return get_chain_runtime_config_snapshot().movie_subscribe_url
@staticmethod
def __subscribe_report_payload(context: _SubscribePostCommitContext) -> dict:
@@ -2938,11 +2941,11 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
subscribeoper.delete(subscribe.id)
# 发送通知
if mediainfo.type == MediaType.TV:
link = settings.MP_DOMAIN('#/subscribe/tv?tab=mysub')
link = self.runtime_config.television_subscribe_url
elif mediainfo.type == MediaType.MUSIC:
link = settings.MP_DOMAIN('#/subscribe/music?tab=mysub')
link = self.runtime_config.music_subscribe_url
else:
link = settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
link = self.runtime_config.movie_subscribe_url
# 完成订阅按规则发送消息
self.post_message(
_SchemaMessage(
@@ -3195,11 +3198,8 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
if not default_subscribe_key:
return None
# 默认订阅规则
if hasattr(settings, default_subscribe_key):
value = getattr(settings, default_subscribe_key)
else:
value = _system_config().get(default_subscribe_key)
# 默认订阅规则属于持久化用户配置,不再从部署 Settings 猜测同名属性。
value = _system_config().get(default_subscribe_key)
if not value:
return None
@@ -3259,7 +3259,10 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
info = _SchemaSubscribeEpisodeInfo()
info.title = episode.name
info.description = episode.overview
info.backdrop = settings.TMDB_IMAGE_URL(episode.still_path, "w500")
info.backdrop = self.runtime_config.tmdb_image_url(
episode.still_path,
"w500",
)
episodes[episode.episode_number] = info
elif subscribe.type == MediaType.TV.value:
# 根据开始结束集计算集信息
+14 -11
View File
@@ -7,7 +7,7 @@ from pathlib import Path
from typing import Union, Optional
from app.chain import ChainBase
from app.runtime.config import settings
from app.application.configuration import get_chain_runtime_config_snapshot
from app.runtime.state import SystemHelper
from app.runtime.log import logger
from app.schemas.message import Message
@@ -70,8 +70,9 @@ class SystemChain(ChainBase):
try:
# 使用绝对路径确保准确性
plugins_dir = settings.ROOT_PATH / "app" / "plugins"
backup_dir = settings.CONFIG_PATH / "plugins_backup"
config = get_chain_runtime_config_snapshot()
plugins_dir = config.root_path / "app" / "plugins"
backup_dir = config.config_path / "plugins_backup"
if not plugins_dir.exists():
logger.info("插件目录不存在,跳过备份")
@@ -134,8 +135,9 @@ class SystemChain(ChainBase):
return
# 使用绝对路径确保准确性
plugins_dir = settings.ROOT_PATH / "app" / "plugins"
backup_dir = settings.CONFIG_PATH / "plugins_backup"
config = get_chain_runtime_config_snapshot()
plugins_dir = config.root_path / "app" / "plugins"
backup_dir = config.config_path / "plugins_backup"
if not backup_dir.exists():
logger.info("插件备份目录不存在,跳过恢复")
@@ -367,8 +369,8 @@ class SystemChain(ChainBase):
try:
# 获取所有发布的版本列表
response = RequestUtils(
proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS
proxies=get_chain_runtime_config_snapshot().proxy,
headers=get_chain_runtime_config_snapshot().github_headers,
).get_res("https://api.github.com/repos/jxxghp/MoviePilot/releases")
if response:
releases = [release['tag_name'] for release in response.json()]
@@ -394,8 +396,8 @@ class SystemChain(ChainBase):
try:
# 获取所有发布的版本列表
response = RequestUtils(
proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS
proxies=get_chain_runtime_config_snapshot().proxy,
headers=get_chain_runtime_config_snapshot().github_headers,
).get_res("https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases")
if response:
releases = [release['tag_name'] for release in response.json()]
@@ -426,9 +428,10 @@ class SystemChain(ChainBase):
获取前端版本
"""
if SystemUtils.is_frozen() and SystemUtils.is_windows():
version_file = settings.CONFIG_PATH.parent / "nginx" / "html" / "version.txt"
config = get_chain_runtime_config_snapshot()
version_file = config.config_path.parent / "nginx" / "html" / "version.txt"
else:
version_file = Path(settings.FRONTEND_PATH) / "version.txt"
version_file = get_chain_runtime_config_snapshot().frontend_path / "version.txt"
if version_file.exists():
try:
with open(version_file, 'r', encoding='utf-8', errors='replace') as f:
+17 -17
View File
@@ -13,7 +13,7 @@ from app.chain import ChainBase
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.tmdb import TmdbChain
from app.runtime.config import settings, global_vars
from app.runtime.config import global_vars
from app.domain.context import MediaInfo, MusicInfo
from app.runtime.events import eventmanager
from app.domain.meta.metabase import MetaBase
@@ -112,11 +112,11 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
"""初始化文件整理处理链。"""
super().__init__()
# 主要媒体文件后缀
self._media_exts = settings.RMT_MEDIAEXT
self._media_exts = self.runtime_config.video_extensions
# 字幕文件后缀
self._subtitle_exts = settings.RMT_SUBEXT
self._subtitle_exts = self.runtime_config.subtitle_extensions
# 音频文件后缀
self._audio_exts = settings.RMT_AUDIOEXT
self._audio_exts = self.runtime_config.audio_extensions
# 可处理的文件后缀(视频文件、字幕、音频文件)
self._allowed_exts = self._media_exts + self._audio_exts + self._subtitle_exts
# 待整理任务队列
@@ -154,7 +154,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
启动文件整理线程
"""
self._queue_active = True
for i in range(settings.TRANSFER_THREADS):
for i in range(self.runtime_config.transfer_threads):
logger.info(f"启动文件整理线程 {i + 1} ...")
thread = threading.Thread(
target=self.__start_transfer, name=f"transfer-{i}", daemon=True
@@ -341,8 +341,8 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
# AI智能体自动重试整理
if (
history
and settings.AI_AGENT_ENABLE
and settings.AI_AGENT_RETRY_TRANSFER
and self.runtime_config.ai_agent_enable
and self.runtime_config.ai_agent_retry_transfer
):
try:
# 使用 download_hash 或源文件父目录作为分组键,
@@ -554,7 +554,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
username=task.username,
manual_identity=manual_identity,
)
if not settings.TRANSFER_FAILURE_NOTIFICATION_AGGREGATION:
if not self.runtime_config.transfer_failure_notification_aggregation:
self._send_transfer_failure_notifications([notification])
return
try:
@@ -616,7 +616,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
text = "\n".join(text_parts)
buttons = [[{
"text": "批量处理",
"url": settings.MP_DOMAIN("#/history"),
"url": self.runtime_config.history_url,
}]]
title = f"{first.media_title} 入库失败({len(notifications)} 个文件)"
self.post_message(
@@ -626,7 +626,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
text=text,
image=first.image,
username=first.username,
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
buttons=buttons,
)
)
@@ -855,7 +855,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
def __expire_stale_transfer_tasks(self):
"""清理外部接管后失去状态心跳的运行中整理任务。"""
timeout_minutes = max(int(settings.TRANSFER_TASK_TIMEOUT), 0)
timeout_minutes = max(int(self.runtime_config.transfer_task_timeout), 0)
expire_tasks = getattr(self.jobview, "expire_stale_running_tasks", None)
expired_tasks = (
expire_tasks(timeout_seconds=timeout_minutes * 60)
@@ -1110,8 +1110,8 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
# AI智能体自动重试整理
if (
his
and settings.AI_AGENT_ENABLE
and settings.AI_AGENT_RETRY_TRANSFER
and self.runtime_config.ai_agent_enable
and self.runtime_config.ai_agent_retry_transfer
):
try:
# 使用 download_hash 或源文件父目录作为分组键
@@ -1136,7 +1136,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
# 只有 TMDB 主源沿用历史 TMDB 标题,避免辅助 ID 改写其它识别源标题。
if (
not settings.SCRAP_FOLLOW_TMDB
not self.runtime_config.scrape_follow_tmdb
and mediainfo.media_source == MediaSource.TMDB
):
transfer_history = transferhis.get_by_media_identity(
@@ -2533,7 +2533,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
source=source,
text=errmsg,
userid=userid,
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -2570,7 +2570,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
source=source,
text=errmsg,
userid=userid,
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
save_history=False,
)
)
@@ -2734,7 +2734,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
ctype=ContentType.OrganizeSuccess,
image=mediainfo.get_message_image(),
username=username,
link=settings.MP_DOMAIN("#/history"),
link=self.runtime_config.history_url,
),
meta=meta,
mediainfo=mediainfo,