refactor: strengthen architecture CI gates and runtime contracts

This commit is contained in:
jxxghp
2026-08-25 17:02:29 +08:00
parent 605bf8174a
commit b914e8b6e4
123 changed files with 4240 additions and 1342 deletions
+14 -15
View File
@@ -5,7 +5,7 @@ import traceback
from abc import ABCMeta
from collections.abc import Callable
from pathlib import Path
from typing import Optional, Any, Tuple, List, Set, Union, Dict
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from app.application.chain.context import ChainRuntimeContext, get_chain_runtime_context
from app.application.chain.data import get_chain_data_ports
@@ -18,24 +18,22 @@ from app.chain._recognition import RecognitionMixin
from app.domain.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.runtime.log import logger
from app.schemas.exception import RateLimitExceededException
from app.schemas.transfer import TransferInfo
from app.schemas.mediaserver import ExistMediaInfo
from app.schemas.transfer import DownloaderFile, DownloaderTorrent
from app.schemas.message import IncomingMessage
from app.schemas.mediaserver import WebhookEventInfo
from app.schemas.tmdb import TmdbEpisode
from app.schemas.context import MediaPerson
from app.schemas.workflow import FileItem
from app.schemas.system import TransferDirectoryConf
from app.schemas.category import CategoryConfig
from app.schemas.context import MediaPerson
from app.schemas.exception import RateLimitExceededException
from app.schemas.mediaserver import ExistMediaInfo, WebhookEventInfo
from app.schemas.message import IncomingMessage
from app.schemas.system import TransferDirectoryConf
from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import DownloaderFile, DownloaderTorrent, TransferInfo
from app.schemas.types import (
TorrentStatus,
MediaType,
MediaSourceSelection,
MediaImageType,
EventType,
MediaImageType,
MediaSourceSelection,
MediaType,
TorrentStatus,
)
from app.schemas.workflow import FileItem
class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
@@ -57,6 +55,7 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
self.filecache = context.file_cache
self.async_filecache = context.async_file_cache
self.runtime_config = context.configuration
self.stop_state = context.stop_state
self.data_ports = context.data_ports or get_chain_data_ports()
self.durable_event_writer = context.durable_event_writer
self._module_dispatcher = context.module_dispatcher_factory(
+96
View File
@@ -0,0 +1,96 @@
"""Chain mixin 对宿主能力的静态契约。"""
from __future__ import annotations
from typing import Any, Protocol
class ChainRuntimeMixinHost(Protocol):
"""识别与消息 mixin 共用的 Chain 运行能力。"""
runtime_config: Any
eventmanager: Any
messageoper: Any
messagequeue: Any
def run_module(self, method: str, **kwargs: Any) -> Any:
"""调用同步模块能力。"""
...
async def async_run_module(self, method: str, **kwargs: Any) -> Any:
"""调用异步模块能力。"""
...
class MusicSubscribeMixinHost(Protocol):
"""音乐订阅 mixin 对 SubscribeChain 的最小要求。"""
@classmethod
def _music_media_chain(cls) -> Any:
"""构造媒体识别链。"""
...
def _music_download_chain(self) -> Any:
"""构造下载链。"""
...
def _music_search_chain(self) -> Any:
"""构造搜索链。"""
...
def _music_site_keywords(self, mediainfo: Any) -> list[str]:
"""构造音乐站点搜索关键字。"""
...
def _matches_music_resource(self, mediainfo: Any, *texts: Any) -> bool:
"""判断站点资源文本是否匹配音乐目标。"""
...
def get_sub_sites(self, subscribe: Any) -> list[int]: ...
def get_params(self, subscribe: Any) -> Any: ...
def filter_torrents(self, *args: Any, **kwargs: Any) -> Any: ...
def check_and_handle_existing_media(self, *args: Any, **kwargs: Any) -> Any: ...
def finish_subscribe_or_not(self, *args: Any, **kwargs: Any) -> Any: ...
def get_subscribe_source_keyword(self, subscribe: Any) -> str: ...
class InteractionMixinHost(Protocol):
"""交互委托 mixin 对业务 Chain 的最小要求。"""
_interaction_handler_type: type
def _interaction_handler(self) -> Any:
"""构造业务交互处理器。"""
...
class TransferMixinHost(ChainRuntimeMixinHost, Protocol):
"""整理辅助 mixin 对 TransferChain 的最小要求。"""
@classmethod
def _transfer_media_chain(cls) -> Any:
"""构造媒体识别链。"""
...
@classmethod
def _transfer_storage_chain(cls) -> Any:
"""构造存储链。"""
...
@classmethod
def _transfer_subscribe_chain(cls) -> Any:
"""构造订阅链。"""
...
def post_message(self, *args: Any, **kwargs: Any) -> Any: ...
async def async_post_message(self, *args: Any, **kwargs: Any) -> Any: ...
def obtain_images(self, *args: Any, **kwargs: Any) -> Any: ...
def do_transfer(self, *args: Any, **kwargs: Any) -> Any: ...
+2
View File
@@ -1,9 +1,11 @@
from typing import Optional, Tuple, Union
from app.chain._contracts import InteractionMixinHost
from app.schemas.types import NotificationChannel
class InteractionChainMixin:
__mixin_host_protocol__ = InteractionMixinHost
"""
斜杠命令交互四件套委托:remote_list / parse_callback /
handle_callback_interaction / handle_text_interaction。
+7 -5
View File
@@ -9,20 +9,21 @@ from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from app.application.chain.data import get_chain_user_port
from app.application.messaging.message import MessageTemplateHelper
from app.application.notification import get_notification_switch
from app.chain._contracts import ChainRuntimeMixinHost
from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.foundation.identity import normalize_internal_user_id
from app.application.messaging.message import MessageTemplateHelper
from app.application.notification import get_notification_switch
from app.runtime.log import logger
from app.schemas.message import MessageResponse
from app.schemas.message import Message
from app.schemas.transfer import TransferInfo
from app.schemas.message import Message, MessageResponse
from app.schemas.notification import ChannelCapability, ChannelCapabilityManager
from app.schemas.transfer import TransferInfo
from app.schemas.types import EventType, NotificationChannel
class MessageProcessingMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
"""消息输入/处理状态机与通知派发规范化。"""
def start_message_processing_status(
@@ -117,6 +118,7 @@ class MessageProcessingMixin:
class NotificationMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
"""通知消息发送域:渲染、隔离路由、队列发送与消息编辑。"""
def post_message(
+10 -3
View File
@@ -1,16 +1,22 @@
import copy
from typing import Any, List, Optional, Tuple
from app.application.torrent import TorrentHelper
from app.application.chain.data import get_chain_subscribe_port
from app.application.configuration import get_configured_system_config
from app.application.subscription.contract import (
build_subscribe_meta,
subscribe_media_key,
)
from app.application.torrent import TorrentHelper
from app.chain._contracts import MusicSubscribeMixinHost
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.application.chain.data import get_chain_subscribe_port
from app.application.configuration import get_configured_system_config
# 旧测试与插件补丁入口;正式依赖通过宿主工厂逐步收敛。
MediaChain = MediaChain
DownloadChain = DownloadChain
SearchChain = SearchChain
from app.domain.context import Context, MediaInfo, MusicInfo
from app.domain.media import MUSIC_SUBSCRIBABLE_TYPES
from app.domain.meta.metamusic import MetaMusic
@@ -35,6 +41,7 @@ def _normalize_music_total_tracks(value: Any) -> Optional[int]:
class MusicSubscribeMixin:
__mixin_host_protocol__ = MusicSubscribeMixinHost
"""
音乐订阅功能域 mixin:单曲/专辑目标识别、实体快照同步、候选筛选、
择优下载与完成推进。
+4 -2
View File
@@ -7,20 +7,22 @@
import copy
from typing import Optional
from app.runtime.execution import run_in_threadpool
from app.adapters.external.server import MoviePilotServerHelper
from app.application.configuration import get_configured_system_config
from app.chain._contracts import ChainRuntimeMixinHost
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.runtime.cache import fresh, async_fresh
from app.runtime.cache import async_fresh, fresh
from app.runtime.events import Event
from app.runtime.execution import run_in_threadpool
from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.types import ChainEventType, MediaSource, MediaType, SystemConfigKey
class RecognitionMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
def _can_use_media_recognize_share(
self,
+22 -10
View File
@@ -7,21 +7,12 @@ TransferChain 中。mixin 方法运行时经 MRO 解析,共享 TransferChain
注意:这里的方法均已去掉私有名前缀双下划线(__ -> _),因为 Python 的名字
改编按定义类生效,方法迁到 mixin 后 __ 前缀会改变改编目标,导致跨类调用失败。
"""
import asyncio
from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from app.schemas.history import DownloadHistory as _SchemaDownloadHistory
from app.schemas.transfer import EpisodeFormatRule as _SchemaEpisodeFormatRule
from app.adapters.system.host import SystemUtils
from app.application.agent import build_manual_redo_prompt, get_running_agent_manager
from app.application.formatting import EpisodeFormatRuleHelper
from app.application.history import clear_transfer_failures, resolve_history
from app.application.transfer import TransferTask, job_lock
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.subscribe import SubscribeChain
from app.application.chain.data import (
get_chain_download_history_port,
get_chain_transfer_history_port,
@@ -30,6 +21,18 @@ from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.formatting import EpisodeFormatRuleHelper
from app.application.history import clear_transfer_failures, resolve_history
from app.application.transfer import TransferTask, job_lock
from app.chain._contracts import TransferMixinHost
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.subscribe import SubscribeChain
# 旧测试与插件补丁入口;正式依赖通过宿主工厂逐步收敛。
MediaChain = MediaChain
StorageChain = StorageChain
SubscribeChain = SubscribeChain
from app.domain.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase
@@ -38,9 +41,10 @@ from app.foundation import text as text_tools
from app.runtime.config import global_vars
from app.runtime.log import logger
from app.runtime.tasks import get_task_registry
from app.schemas.workflow import FileItem
from app.schemas.history import DownloadHistory as _SchemaDownloadHistory
from app.schemas.message import Message
from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import EpisodeFormatRule as _SchemaEpisodeFormatRule
from app.schemas.transfer import TransferInfo
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -51,6 +55,7 @@ from app.schemas.types import (
ReplyMode,
SystemConfigKey,
)
from app.schemas.workflow import FileItem
DownloadFiles = Any
DownloadHistory = Any
@@ -100,6 +105,7 @@ SUBTITLE_STEM_TAGS = {
class FileFilterMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod
def _requires_automatic_category(task: TransferTask) -> bool:
"""
@@ -446,6 +452,7 @@ class FileFilterMixin:
class ScrapeBatchMixin:
__mixin_host_protocol__ = TransferMixinHost
def _send_metadata_scrape_event(
self, task: TransferTask, transferinfo: TransferInfo
@@ -647,6 +654,7 @@ class ScrapeBatchMixin:
class EpisodeFormatMixin:
__mixin_host_protocol__ = TransferMixinHost
def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Optional[str]:
"""
@@ -837,6 +845,7 @@ class EpisodeFormatMixin:
class HistoryMatchMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod
def _match_download_file(
download_file: DownloadFiles,
@@ -1023,6 +1032,7 @@ class HistoryMatchMixin:
class FileKeyMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod
def _get_file_key(fileitem: FileItem) -> Tuple[str, str]:
"""
@@ -1110,6 +1120,7 @@ class FileKeyMixin:
class ManualHistoryMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod
def _get_subscribe_custom_words(
history_record: Optional[DownloadHistory],
@@ -1234,6 +1245,7 @@ class ManualHistoryMixin:
class FailedRetryMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod
def build_failed_transfer_buttons(
history_id: Optional[int],
+1 -1
View File
@@ -1,8 +1,8 @@
from typing import Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.chain import ChainBase
from app.domain.context import MediaInfo
from app.schemas.context import MediaPerson as _SchemaMediaPerson
class AniListChain(ChainBase):
+2 -2
View File
@@ -1,8 +1,8 @@
from typing import Optional, List
from typing import List, Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.chain import ChainBase
from app.domain.context import MediaInfo
from app.schemas.context import MediaPerson as _SchemaMediaPerson
class BangumiChain(ChainBase):
+2 -2
View File
@@ -1,8 +1,8 @@
from typing import Optional, List
from typing import List, Optional
from app.chain import ChainBase
from app.schemas.dashboard import DownloaderInfo as _SchemaDownloaderInfo
from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.chain import ChainBase
class DashboardChain(ChainBase):
+1 -1
View File
@@ -1,9 +1,9 @@
from typing import Any, List, Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.chain import ChainBase
from app.domain.context import MediaInfo, MusicAlbumInfo, MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType
+44 -37
View File
@@ -6,18 +6,25 @@ import re
import shutil
import time
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple, Set, Dict, Union
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from urllib.parse import parse_qs, urlencode, urljoin, urlparse
from app.schemas.transfer import DownloaderTorrent as _SchemaDownloaderTorrent
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.adapters.network.http import RequestUtils
from app.adapters.system.host import SystemUtils
from app.application.chain.data import (
get_chain_download_failure_port,
get_chain_download_history_port,
get_chain_media_server_port,
)
from app.application.configuration import get_chain_runtime_config_snapshot
from app.application.directory import DirectoryHelper, validate_download_save_path
from app.application.download import selection as _selection
from app.application.download.tasks import DownloadTaskService
from app.application.torrent import TorrentHelper
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 global_vars
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain import episode as episode_rules
from app.domain.context import (
Context,
MediaInfo,
@@ -25,36 +32,36 @@ from app.domain.context import (
SubtitleInfo,
TorrentInfo,
)
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
from app.application.chain.data import (
get_chain_download_failure_port,
get_chain_download_history_port,
get_chain_media_server_port,
)
from app.application.directory import DirectoryHelper, validate_download_save_path
from app.application.download.tasks import DownloadTaskService
from app.application.download import selection as _selection
from app.runtime.thread import ThreadHelper
from app.application.torrent import TorrentHelper
from app.runtime.log import logger
from app.schemas.mediaserver import ExistMediaInfo
from app.schemas.file import FileURI
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.transfer import DownloaderTorrent
from app.schemas.message import Message
from app.schemas.event import ResourceSelectionEventData
from app.schemas.event import ResourceDownloadEventData
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType, TorrentStatus, EventType, NotificationChannel, MessageType, ContentType, \
ChainEventType
from app.adapters.network.http import RequestUtils
from app.schemas.media import build_media_key, resolve_media_identity
from app.domain import episode as episode_rules
from app.foundation import size as size_tools
from app.foundation import text as text_tools
from app.adapters.system.host import SystemUtils
from app.runtime.cache import FileCache
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.runtime.thread import ThreadHelper
from app.schemas.event import ResourceDownloadEventData, ResourceSelectionEventData
from app.schemas.file import FileURI
from app.schemas.media import build_media_key, resolve_media_identity
from app.schemas.mediaserver import ExistMediaInfo, NotExistMediaInfo
from app.schemas.message import Message
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
from app.schemas.transfer import DownloaderTorrent
from app.schemas.transfer import DownloaderTorrent as _SchemaDownloaderTorrent
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
ChainEventType,
ContentType,
EventType,
MediaSource,
MediaType,
MessageType,
NotificationChannel,
TorrentStatus,
)
from app.schemas.workflow import FileItem as _SchemaFileItem
if TYPE_CHECKING:
from typing import Any
@@ -1000,7 +1007,7 @@ class DownloadChain(ChainBase):
MediaType.MUSIC: set(),
}
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
media_type = context.media_info.type
if media_type not in downloaded_keys:
@@ -1657,7 +1664,7 @@ class DownloadChain(ChainBase):
for need_mid, need_season in need_seasons.items():
# 循环种子
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
# 媒体信息
media = context.media_info
@@ -1801,7 +1808,7 @@ class DownloadChain(ChainBase):
need_episodes = list(range(start_episode, total_episode + 1))
# 循环种子
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
# 媒体信息
media = context.media_info
@@ -1889,7 +1896,7 @@ class DownloadChain(ChainBase):
continue
# 循环种子
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
# 媒体信息
media = context.media_info
@@ -2040,7 +2047,7 @@ class DownloadChain(ChainBase):
media_id=media_id,
episode_group=mediainfo.episode_group)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
logger.error("媒体信息识别失败!")
return False, {}
if not mediainfo.seasons:
logger.error(f"媒体信息中没有季集信息:{mediainfo.title_year}")
+7 -7
View File
@@ -2,18 +2,18 @@ import math
import re
from typing import Any, Dict, List, Optional, Tuple, Union
from app.chain import ChainBase
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.subscribe import SubscribeChain
from app.application.chain.data import get_chain_user_port
from app.application.directory import DirectoryHelper
from app.application.messaging.media import (
PendingMediaInteraction,
media_interaction_manager,
)
from app.application.torrent import TorrentHelper
from app.application.chain.data import get_chain_user_port
from app.chain import ChainBase
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.subscribe import SubscribeChain
from app.domain import episode as episode_rules
from app.domain import title as title_rules
from app.domain.context import Context, MediaInfo
@@ -22,9 +22,9 @@ from app.foundation import url as url_tools
from app.runtime.log import logger
from app.schemas.download import DownloadDirectory
from app.schemas.file import FileURI
from app.schemas.media import build_media_key, resolve_media_identity
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.message import Message
from app.schemas.media import build_media_key, resolve_media_identity
from app.schemas.notification import ChannelCapabilityManager
from app.schemas.system import TransferDirectoryConf
from app.schemas.types import MediaType, NotificationChannel
+16 -16
View File
@@ -3,15 +3,15 @@ from pathlib import Path
from threading import Lock
from typing import Any, Iterable, List, Optional, Tuple, Union
from app.runtime.execution import run_in_threadpool
from app.schemas.event import MediaRecognizeConvertEventData as _SchemaMediaRecognizeConvertEventData
from app.application.audio import AudioMetadataHelper
from app.application.configuration import get_chain_runtime_config_snapshot
from app.application.music.catalog import MusicCatalogService
from app.chain import ChainBase
from app.chain.acoustid import AcoustIdChain
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.application.configuration import get_chain_runtime_config_snapshot
from app.domain import title as title_rules
from app.domain.context import (
Context,
MediaInfo,
@@ -19,13 +19,18 @@ from app.domain.context import (
MusicArtistInfo,
MusicInfo,
)
from app.runtime.events import Event
from app.domain.media import is_music_media_source
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.application.audio import AudioMetadataHelper
from app.application.music.catalog import MusicCatalogService
from app.foundation.singleton import Singleton
from app.foundation.text import convert as zhconv_convert
from app.runtime.cache import async_fresh, fresh
from app.runtime.events import Event
from app.runtime.execution import run_in_threadpool
from app.runtime.log import logger
from app.schemas.event import MediaRecognizeConvertEventData as _SchemaMediaRecognizeConvertEventData
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.types import (
MUSIC_ENTITY_RECORDING,
ChainEventType,
@@ -33,11 +38,6 @@ from app.schemas.types import (
MediaSourceSelection,
MediaType,
)
from app.domain.media import is_music_media_source
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.foundation.singleton import Singleton
from app.foundation.text import convert as zhconv_convert
from app.domain import title as title_rules
recognize_lock = Lock()
@@ -786,9 +786,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
year = None
# 结果赋值
if title == org_meta.name and year == org_meta.year:
logger.info(f"辅助识别与原始识别结果一致,无需重新识别媒体信息")
logger.info("辅助识别与原始识别结果一致,无需重新识别媒体信息")
return None
logger.info(f"辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
logger.info("辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
org_meta.name = title
org_meta.year = year
org_meta.begin_season = season_number
@@ -1725,9 +1725,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
year = None
# 结果赋值
if title == org_meta.name and year == org_meta.year:
logger.info(f"辅助识别与原始识别结果一致,无需重新识别媒体信息")
logger.info("辅助识别与原始识别结果一致,无需重新识别媒体信息")
return None
logger.info(f"辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
logger.info("辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
org_meta.name = title
org_meta.year = year
org_meta.begin_season = season_number
+8 -11
View File
@@ -1,18 +1,15 @@
import threading
from datetime import datetime
from typing import Callable, Dict, List, Union, Optional, Generator, Any, Tuple
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
from app.chain import ChainBase
from app.runtime.config import global_vars
from app.application.chain.data import get_chain_media_server_port
from app.application.mediaserver import get_mediaserver_configs
from app.runtime.log import logger
from app.schemas.mediaserver import MediaServerLibrary
from app.schemas.mediaserver import MediaServerItem
from app.schemas.mediaserver import MediaServerSeasonInfo
from app.schemas.mediaserver import MediaServerPlayItem
from app.schemas.types import MediaType
from app.application.security.url import SecurityUtils
from app.chain import ChainBase
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.mediaserver import MediaServerItem, MediaServerLibrary, MediaServerPlayItem, MediaServerSeasonInfo
from app.schemas.types import MediaType
lock = threading.Lock()
@@ -355,7 +352,7 @@ class MediaServerChain(ChainBase):
library_media_total = library_media_counts.get(str(library.id))
library_count = 0
for item in self.items(server=server_name, library_id=library.id):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
return total_count, global_media_finished
if not item or not item.item_id:
continue
@@ -554,7 +551,7 @@ class MediaServerChain(ChainBase):
global_media_total=global_media_total,
global_media_finished=global_media_finished,
)
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
return
total_count += server_count
logger.info(f"媒体服务器 {server_name} 数据同步完成,总同步数量:{total_count}")
+10 -11
View File
@@ -7,22 +7,16 @@ from concurrent.futures import CancelledError as FutureCancelledError
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Optional, Dict, Union, List, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import unquote, urlparse
from app.adapters.network.http import RequestUtils
from app.application.agent import (
get_running_agent_manager,
is_audio_input_available,
supports_image_input,
transcribe_audio,
)
from app.chain import ChainBase
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 global_vars
from app.runtime.tasks import get_task_registry
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
@@ -32,12 +26,17 @@ from app.application.messaging.session import MessageSessionService
from app.application.messaging.site import site_interaction_manager
from app.application.messaging.skill import SkillInteractionHandler, skill_interaction_manager
from app.application.messaging.subscribe import subscribe_interaction_manager
from app.chain import ChainBase
from app.chain.interaction import MediaInteractionChain as _MediaInteractionChain
from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain
from app.runtime.config import global_vars
from app.runtime.log import logger
from app.schemas.message import IncomingMessage
from app.schemas.message import Message
from app.runtime.tasks import get_task_registry
from app.schemas.message import IncomingMessage, Message
from app.schemas.notification import ChannelCapabilityManager
from app.schemas.types import EventType, NotificationChannel
from app.adapters.network.http import RequestUtils
class MessageChain(ChainBase):
+8 -8
View File
@@ -2,25 +2,25 @@ from typing import Callable, List, Optional
import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用
from app.application.image import ImageHelper
from app.chain import ChainBase
from app.chain.bangumi import BangumiChain
from app.chain.douban import DoubanChain
from app.chain.listenbrainz import ListenBrainzChain
from app.chain.tmdb import TmdbChain
from app.runtime.cache import cached, fresh
from app.runtime.config import global_vars
from app.domain.context import MusicInfo
from app.application.image import ImageHelper
from app.foundation.singleton import Singleton
from app.runtime.cache import cached, fresh
from app.runtime.execution import log_execution_time
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.media import normalize_media_source
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING,
MediaSource,
MediaType,
)
from app.runtime.execution import log_execution_time
from app.schemas.media import normalize_media_source
from app.foundation.singleton import Singleton
class RecommendChain(ChainBase, metaclass=Singleton):
@@ -222,7 +222,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
# 这里避免区间内连续调用相同来源,因此遍历方案为每页遍历所有推荐来源,再进行页数遍历
for page in range(1, self.cache_max_pages + 1):
for method in recommend_methods:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
return
if method in methods_finished:
continue
@@ -277,7 +277,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
total_num = len(datas)
for index, data in enumerate(datas, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
return
poster_path = data.get("poster_path")
if poster_path:
+15 -18
View File
@@ -7,47 +7,44 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory
from threading import Lock
from typing import Any, Iterable, List, Optional, Tuple, Union
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.adapters.network.http import RequestUtils
from app.application.audio import AudioMetadataHelper
from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.chain import ChainBase
from app.chain.lrclib import LrclibChain
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.runtime.cache import cached
from app.domain.context import (
MediaInfo,
MusicAlbumInfo,
MusicInfo,
MusicLyrics,
)
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_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.audio import AudioMetadataHelper
from app.foundation.singleton import Singleton
from app.runtime.cache import cached
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.schemas.workflow import FileItem
from app.runtime.reload import ConfigReloadMixin
from app.schemas.media import resolve_media_identity
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING,
EventType,
MediaSource,
MediaType,
ScrapingTarget,
ScrapingMetadata,
ScrapingPolicy,
ScrapingTarget,
SystemConfigKey,
)
from app.adapters.network.http import RequestUtils
from app.schemas.media import resolve_media_identity
from app.runtime.reload import ConfigReloadMixin
from app.foundation.singleton import Singleton
from app.chain.media import MediaChain
from app.schemas.workflow import FileItem
from app.schemas.workflow import FileItem as _SchemaFileItem
scraping_lock = Lock()
+22 -25
View File
@@ -7,34 +7,34 @@ import time
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, as_completed, wait
from contextlib import aclosing
from datetime import datetime
from typing import AsyncIterator, Any, Awaitable, Callable, Dict, Iterable, Tuple
from typing import List, Optional
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterable, List, Optional, Tuple
from unicodedata import normalize
from app.runtime.execution import run_in_threadpool, submit_with_context
from app.chain import ChainBase
from app.chain.media import MediaChain
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_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.runtime.tasks import get_task_registry
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.search.state import (
SearchStateService,
normalize_search_params,
stringify_sites,
)
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.torrent import TorrentHelper
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.foundation import size as size_tools
from app.foundation.text import convert as zhconv_convert
from app.runtime.events import Event, eventmanager
from app.runtime.execution import run_in_threadpool, submit_with_context
from app.runtime.log import logger
from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.runtime.stop import runtime_stop_state
from app.runtime.tasks import get_task_registry
from app.schemas.media import build_media_key, resolve_media_identity
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -44,9 +44,6 @@ from app.schemas.types import (
ProgressKey,
SystemConfigKey,
)
from app.schemas.media import build_media_key, resolve_media_identity
from app.foundation import size as size_tools
from app.foundation.text import convert as zhconv_convert
class SearchChain(ChainBase):
@@ -1355,7 +1352,7 @@ class SearchChain(ChainBase):
logger.info(f"开始匹配结果 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
progress.update(value=51, text=f'开始匹配,总 {_total} 个资源 ...')
for torrent in torrents:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
_count += 1
progress.update(value=(_count / _total) * 96,
@@ -1709,7 +1706,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo),
)
if not mediainfo:
logger.error(f'媒体信息识别失败!')
logger.error('媒体信息识别失败!')
return []
# 准备搜索参数
@@ -1802,7 +1799,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo),
)
if not mediainfo:
logger.error(f'媒体信息识别失败!')
logger.error('媒体信息识别失败!')
return []
# 准备搜索参数
@@ -1885,7 +1882,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo),
)
if not mediainfo:
logger.error(f'媒体信息识别失败!')
logger.error('媒体信息识别失败!')
yield {
"type": "error",
"success": False,
@@ -2071,7 +2068,7 @@ class SearchChain(ChainBase):
match_subtitles = []
logger.info(f"开始匹配字幕 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
for subtitle in subtitles:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
subtitle_names = self.__build_subtitle_names(subtitle)
if not subtitle_names:
@@ -2384,7 +2381,7 @@ class SearchChain(ChainBase):
try:
while pending_tasks:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
done_tasks, _ = wait(pending_tasks, return_when=FIRST_COMPLETED)
for future in done_tasks:
@@ -2460,7 +2457,7 @@ class SearchChain(ChainBase):
try:
while pending_tasks:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
done_tasks, _ = await asyncio.wait(
pending_tasks,
+23 -23
View File
@@ -1,35 +1,35 @@
import base64
import re
from datetime import datetime
from typing import Any, Callable, Optional, Tuple, Union, Dict
from typing import Any, Callable, Dict, Optional, Tuple, Union
from urllib.parse import urljoin
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from lxml import etree
from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.application.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config
from app.adapters.external.cookiecloud import CookieCloudHelper
from app.adapters.network.browser import PlaywrightHelper
from app.adapters.network.cloudflare import under_challenge
from app.application.security.cookie import CookieHelper
from app.adapters.external.cookiecloud import CookieCloudHelper
from app.adapters.network.http import RequestUtils
from app.application.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config
from app.application.messaging.site import SiteInteractionHandler
from app.application.rss import RssHelper
from app.runtime.log import logger
from app.schemas.notification import NotificationChannel
from app.schemas.message import Message
from app.schemas.site import SiteUserData
from app.schemas.types import EventType, MessageType
from app.adapters.network.http import RequestUtils
from app.domain.site import SiteUtils
from app.application.security.cookie import CookieHelper
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.domain import site as site_rules
from app.domain.site import SiteUtils
from app.foundation import size as size_tools
from app.foundation import url as url_tools
from app.foundation.dom import DomUtils
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.message import Message
from app.schemas.notification import NotificationChannel
from app.schemas.site import SiteUserData
from app.schemas.types import EventType, MessageType
Site = Any
@@ -83,7 +83,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)):
self.post_message(Message(
mtype=MessageType.SiteMessage,
title=f"【站点分享率低预警】",
title="【站点分享率低预警】",
text=f"站点 {site.get('name')} 分享率 {userdata.ratio},请注意!"
))
return userdata
@@ -140,7 +140,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0},
)
for index, site in enumerate(sites, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
return None
if progress_callback:
progress_callback(
@@ -429,7 +429,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
update_count = add_count = fail_count = 0
for index, (domain, cookie) in enumerate(cookies.items(), start=1):
# 检查系统是否停止
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
logger.info("系统正在停止,中断CookieCloud同步")
return False, "系统正在停止,同步被中断"
if progress_callback:
@@ -708,8 +708,8 @@ class SiteChain(InteractionChainMixin, ChainBase):
timeout=timeout)
if not public and not SiteUtils.is_logged_in(page_source):
if under_challenge(page_source):
return False, f"无法通过Cloudflare"
return False, f"仿真登录失败,Cookie已失效!"
return False, "无法通过Cloudflare"
return False, "仿真登录失败,Cookie已失效!"
else:
res = RequestUtils(cookies=site_cookie,
ua=ua,
@@ -731,7 +731,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
elif res is not None:
return False, f"错误:{res.status_code} {res.reason}"
else:
return False, f"无法打开网站!"
return False, "无法打开网站!"
return True, "连接成功"
def _interaction_handler(self) -> "SiteInteractionHandler":
+3 -3
View File
@@ -1,10 +1,10 @@
from pathlib import Path
from typing import Any, Optional, List, Dict
from typing import Any, Dict, List, Optional
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.chain import ChainBase
from app.application.directory import DirectoryHelper
from app.chain import ChainBase
from app.runtime.log import logger
from app.schemas.workflow import FileItem as _SchemaFileItem
class StorageChain(ChainBase):
+87 -55
View File
@@ -5,35 +5,9 @@ import threading
import time
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Union, Tuple
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from app.schemas.mediaserver import NotExistMediaInfo as _SchemaNotExistMediaInfo
from app.schemas.message import Message as _SchemaMessage
from app.schemas.subscribe import SubscrbieInfo as _SchemaSubscrbieInfo
from app.schemas.subscribe import SubscribeDownloadFileInfo as _SchemaSubscribeDownloadFileInfo
from app.schemas.subscribe import SubscribeEpisodeInfo as _SchemaSubscribeEpisodeInfo
from app.schemas.subscribe import SubscribeLibraryFileInfo as _SchemaSubscribeLibraryFileInfo
from app.schemas.workflow import Subscribe as _SchemaSubscribe
from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.chain._music import MusicSubscribeMixin
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
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 global_vars
from app.domain.context import (
Context,
MediaInfo,
TorrentInfo,
)
from app.runtime.events import eventmanager, Event
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.meta.words import WordsMatcher
from app.domain.metainfo import MetaInfo
from app.adapters.external.server import MoviePilotServerHelper
from app.application.chain.data import (
get_chain_download_history_port,
get_chain_site_port,
@@ -43,30 +17,66 @@ from app.application.configuration import (
get_chain_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.messaging.subscribe import SubscribeInteractionHandler
from app.application.messaging.message import MessageTemplateHelper
from app.application.mediaserver import MediaServerHelper
from app.application.subscription.write import add_subscribe, async_add_subscribe
from app.application.subscription.complete import get_subscription_completion_scope
from app.application.messaging.message import MessageTemplateHelper
from app.application.messaging.subscribe import SubscribeInteractionHandler
from app.application.subscription import priority as _priority
from app.application.subscription.complete import get_subscription_completion_scope
from app.application.subscription.contract import (
build_subscribe_meta as _build_subscribe_meta,
)
from app.application.subscription.contract import (
subscribe_media_key,
subscribe_media_keys,
)
from app.application.subscription.delete import (
SubscribeDeletionActor,
get_sync_delete_subscribe_scope,
)
from app.application.subscription.contract import (
build_subscribe_meta as _build_subscribe_meta,
subscribe_media_key,
subscribe_media_keys,
)
from app.application.subscription.query import SubscriptionQueryService
from app.adapters.external.server import MoviePilotServerHelper
from app.application.subscription.write import add_subscribe, async_add_subscribe
from app.application.torrent import TorrentHelper
from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.chain._music import MusicSubscribeMixin
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
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.domain.context import (
Context,
MediaInfo,
TorrentInfo,
)
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.meta.words import WordsMatcher
from app.domain.metainfo import MetaInfo
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.schemas.event import SubscribeEpisodesRefreshEventData
from app.schemas.event import SubscribeCompletionCheckEventData
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType, SystemConfigKey, NotificationChannel, MessageType, EventType, ChainEventType, \
ContentType
from app.runtime.stop import runtime_stop_state
from app.schemas.event import SubscribeCompletionCheckEventData, SubscribeEpisodesRefreshEventData
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.mediaserver import NotExistMediaInfo as _SchemaNotExistMediaInfo
from app.schemas.message import Message as _SchemaMessage
from app.schemas.subscribe import SubscrbieInfo as _SchemaSubscrbieInfo
from app.schemas.subscribe import SubscribeDownloadFileInfo as _SchemaSubscribeDownloadFileInfo
from app.schemas.subscribe import SubscribeEpisodeInfo as _SchemaSubscribeEpisodeInfo
from app.schemas.subscribe import SubscribeLibraryFileInfo as _SchemaSubscribeLibraryFileInfo
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
ChainEventType,
ContentType,
EventType,
MediaSource,
MediaType,
MessageType,
NotificationChannel,
SystemConfigKey,
)
from app.schemas.workflow import Subscribe as _SchemaSubscribe
if hasattr(_SchemaSubscribe, "model_fields"):
Subscribe = _SchemaSubscribe
@@ -191,6 +201,28 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
电影下载优先级 writer 单独维护
"""
@classmethod
def _music_media_chain(cls):
"""为音乐 mixin 提供可替换的媒体识别构造点。"""
from app.chain import _music as _music_mixin
return (_music_mixin.MediaChain or MediaChain)()
def _music_download_chain(self):
"""为音乐 mixin 提供可替换的下载构造点。"""
from app.chain import _music as _music_mixin
return (_music_mixin.DownloadChain or DownloadChain)()
def _music_search_chain(self):
"""为音乐 mixin 提供可替换的搜索构造点。"""
from app.chain import _music as _music_mixin
return (_music_mixin.SearchChain or SearchChain)()
def _music_site_keywords(self, mediainfo):
return SearchChain.music_site_keywords(mediainfo)
def _matches_music_resource(self, mediainfo, *texts):
return SearchChain.matches_music_resource(mediainfo, *texts)
# 交互处理器类注入,供 InteractionChainMixin 的 parse_callback 委托
_interaction_handler_type = SubscribeInteractionHandler
@@ -1179,7 +1211,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
try:
# 遍历订阅
for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
processed_subscribes.append(subscribe)
if progress_callback:
@@ -1275,7 +1307,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
matched_contexts = []
try:
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
torrent_meta = context.meta_info
torrent_info = context.torrent_info
@@ -1592,11 +1624,11 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
"""预识别待匹配资源,并保留原上下文供后续订阅复用。"""
processed_torrents: Dict[str, List[Context]] = {}
for domain, contexts in torrents.items():
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
processed_torrents[domain] = []
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if context.torrent_info and getattr(context.torrent_info, "category", None) in (
MediaType.MUSIC,
@@ -1699,7 +1731,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
)
try:
for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if progress_callback:
progress_callback(
@@ -1772,13 +1804,13 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
systemconfig = _system_config()
wordsmatcher = WordsMatcher()
for domain, contexts in processed_torrents.items():
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if domains and domain not in domains:
continue
logger.debug(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...')
for context in contexts:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
# 提取信息
_context = copy.copy(context)
@@ -2046,7 +2078,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
)
# 遍历订阅
for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
logger.info(f'开始更新订阅元数据:{subscribe.name} ...')
if progress_callback:
@@ -2174,7 +2206,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
if progress_callback:
progress_callback(value=100, text="未配置 Follow 订阅用户,跳过刷新")
return
logger.info(f'开始刷新follow用户分享订阅 ...')
logger.info('开始刷新follow用户分享订阅 ...')
success_count = 0
subscribeoper = get_chain_subscribe_port()
share_subscribes = MoviePilotServerHelper.get_subscribe_shares() or []
@@ -2186,7 +2218,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0},
)
for index, share_sub in enumerate(share_subscribes, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if progress_callback:
progress_callback(
@@ -2276,7 +2308,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
:param progress_callback: 定时服务进度更新回调
"""
logger.info(f'开始预缓存订阅日历 ...')
logger.info('开始预缓存订阅日历 ...')
subscribes = await get_chain_subscribe_port().async_list()
total_num = len(subscribes)
if progress_callback:
@@ -2286,7 +2318,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0},
)
for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if progress_callback:
progress_callback(
@@ -2336,7 +2368,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
text=f"订阅日历({index}/{total_num})预缓存完成",
data={"total": total_num, "finished": index},
)
logger.info(f'订阅日历预缓存完成')
logger.info('订阅日历预缓存完成')
if progress_callback:
progress_callback(value=100, text="订阅日历预缓存完成")
+8 -8
View File
@@ -4,17 +4,17 @@ import re
import shutil
import uuid
from pathlib import Path
from typing import Union, Optional
from typing import Optional, Union
from app.chain import ChainBase
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
from app.schemas.notification import NotificationChannel
from app.adapters.network.http import RequestUtils
from app.adapters.system.host import SystemUtils
from app.application.configuration import get_chain_runtime_config_snapshot
from app.chain import ChainBase
from app.runtime import version as runtime_version
from app.runtime.log import logger
from app.runtime.state import SystemHelper
from app.schemas.message import Message
from app.schemas.notification import NotificationChannel
class SystemChain(ChainBase):
@@ -33,7 +33,7 @@ class SystemChain(ChainBase):
self.post_message(Message(
channel=channel,
source=source,
title=f"缓存清理完成!",
title="缓存清理完成!",
userid=userid,
save_history=False))
+4 -4
View File
@@ -1,11 +1,11 @@
import random
from typing import Optional, List
from typing import List, Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.tmdb import TmdbSeason as _SchemaTmdbSeason
from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.chain import ChainBase
from app.domain.context import MediaInfo
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.schemas.tmdb import TmdbSeason as _SchemaTmdbSeason
from app.schemas.types import MediaType
+20 -22
View File
@@ -1,27 +1,25 @@
import copy
import re
import traceback
from typing import Callable, Dict, List, Union, Optional
from typing import Callable, Dict, List, Optional, Union
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.runtime.config import global_vars
from app.domain.context import TorrentInfo, Context, MediaInfo
from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.application.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config
from app.application.rss import RssHelper
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.torrent import TorrentHelper
from app.runtime.log import logger
from app.schemas.message import Message
from app.schemas.types import SystemConfigKey, NotificationChannel, MessageType, MediaType
from app.schemas.media import resolve_media_identity
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.domain import site as site_rules
from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.foundation import text as text_tools
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.media import resolve_media_identity
from app.schemas.message import Message
from app.schemas.types import MediaType, MessageType, NotificationChannel, SystemConfigKey
class TorrentsChain(ChainBase):
@@ -50,13 +48,13 @@ class TorrentsChain(ChainBase):
"""
self.post_message(Message(
channel=channel,
title=f"开始刷新种子 ...",
title="开始刷新种子 ...",
userid=userid,
save_history=False))
self.refresh()
self.post_message(Message(
channel=channel,
title=f"种子刷新完成!",
title="种子刷新完成!",
userid=userid,
save_history=False))
@@ -362,23 +360,23 @@ class TorrentsChain(ChainBase):
"""
清理种子缓存数据包含音乐独立缓存
"""
logger.info(f'开始清理种子缓存数据 ...')
logger.info('开始清理种子缓存数据 ...')
self.remove_cache(self._spider_file)
self.remove_cache(self._rss_file)
self.remove_cache(self._music_spider_file)
self.remove_cache(self._music_rss_file)
logger.info(f'种子缓存数据清理完成')
logger.info('种子缓存数据清理完成')
async def async_clear_torrents(self):
"""
异步清理种子缓存数据包含音乐独立缓存
"""
logger.info(f'开始异步清理种子缓存数据 ...')
logger.info('开始异步清理种子缓存数据 ...')
await self.async_remove_cache(self._spider_file)
await self.async_remove_cache(self._rss_file)
await self.async_remove_cache(self._music_spider_file)
await self.async_remove_cache(self._music_rss_file)
logger.info(f'异步种子缓存数据清理完成')
logger.info('异步种子缓存数据清理完成')
def browse(self, domain: str, keyword: Optional[str] = None, cat: Optional[str] = None,
page: Optional[int] = 0,
@@ -585,7 +583,7 @@ class TorrentsChain(ChainBase):
return domain
logger.info(f'{indexer.get("name")}{len(torrents) + len(music_torrents)} 个新种子')
for torrent in torrents + music_torrents:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if not torrent.enclosure:
logger.warning(f"缺少种子链接,忽略处理: {torrent.title}")
@@ -692,7 +690,7 @@ class TorrentsChain(ChainBase):
)
# 遍历站点缓存资源
for index, indexer in enumerate(indexers, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if progress_callback:
progress_callback(
+79 -51
View File
@@ -10,55 +10,38 @@ from concurrent.futures import CancelledError as FutureCancelledError
from concurrent.futures import Future
from copy import deepcopy
from pathlib import Path
from typing import List, Optional, Tuple, Union, Dict, Callable, Any
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
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 global_vars
from app.domain.context import MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfoPath
from app.application.chain.data import (
get_chain_download_history_port,
get_chain_transfer_history_port,
get_chain_transfer_pending_port,
)
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.domain.context import MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfoPath
from app.runtime.config import global_vars
from app.runtime.stop import runtime_stop_state
DownloadHistory = Any
from app.application.configuration import get_configured_system_config
from app.application.directory import DirectoryHelper
from app.application.formatting import FormatParser
from app.runtime.progress import ProgressHelper
from app.application.history import (add_transfer_fail, add_transfer_success,
clear_transfer_failures, describe_history_gate,
evaluate_history_gate, is_skip_action,
record_transfer_failure)
from app.application.outbox import TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC
from app.runtime.log import logger
from app.schemas.event import StorageOperSelectionEventData
from app.schemas.transfer import TransferInfo
from app.schemas.message import Message
from app.schemas.transfer import EpisodeFormat
from app.schemas.workflow import FileItem
from app.schemas.system import TransferDirectoryConf
from app.schemas.transfer import TransferJob
from app.schemas.tmdb import TmdbEpisode
from app.schemas.exception import OperationInterrupted
from app.schemas.types import (
TorrentStatus,
EventType,
MediaType,
ProgressKey,
MessageType,
NotificationChannel,
SystemConfigKey,
ChainEventType,
ContentType,
MediaSource,
from app.application.history import (
add_transfer_fail,
add_transfer_success,
clear_transfer_failures,
describe_history_gate,
evaluate_history_gate,
is_skip_action,
record_transfer_failure,
)
from app.runtime.reload import ConfigReloadMixin
from app.application.outbox import TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC
from app.application.transfer import (
FailedRetryScheduler,
JobManager,
@@ -70,13 +53,40 @@ from app.application.transfer import (
build_transfer_failure_group_key,
job_lock,
)
from app.chain._transfer import (EpisodeFormatMixin, FailedRetryMixin,
FileFilterMixin, FileKeyMixin,
HistoryMatchMixin, ManualHistoryMixin,
ScrapeBatchMixin)
from app.schemas.media import resolve_media_identity
from app.foundation.singleton import Singleton
from app.chain._transfer import (
EpisodeFormatMixin,
FailedRetryMixin,
FileFilterMixin,
FileKeyMixin,
HistoryMatchMixin,
ManualHistoryMixin,
ScrapeBatchMixin,
)
from app.domain import episode as episode_rules
from app.foundation.singleton import Singleton
from app.runtime.log import logger
from app.runtime.progress import ProgressHelper
from app.runtime.reload import ConfigReloadMixin
from app.schemas.event import StorageOperSelectionEventData
from app.schemas.exception import OperationInterrupted
from app.schemas.media import resolve_media_identity
from app.schemas.message import Message
from app.schemas.system import TransferDirectoryConf
from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import EpisodeFormat, TransferInfo, TransferJob
from app.schemas.types import (
ChainEventType,
ContentType,
EventType,
MediaSource,
MediaType,
MessageType,
NotificationChannel,
ProgressKey,
SystemConfigKey,
TorrentStatus,
)
from app.schemas.workflow import FileItem
# 下载器锁
downloader_lock = threading.Lock()
@@ -90,6 +100,24 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
文件整理处理链
"""
@classmethod
def _transfer_media_chain(cls):
"""为整理 mixin 提供可替换的媒体识别构造点。"""
from app.chain import _transfer as _transfer_mixin
return (_transfer_mixin.MediaChain or MediaChain)()
@classmethod
def _transfer_storage_chain(cls):
"""为整理 mixin 提供可替换的存储构造点。"""
from app.chain import _transfer as _transfer_mixin
return (_transfer_mixin.StorageChain or StorageChain)()
@classmethod
def _transfer_subscribe_chain(cls):
"""为整理 mixin 提供可替换的订阅构造点。"""
from app.chain.subscribe import SubscribeChain as _SubscribeChain
return _SubscribeChain()
# worker 在构造期启动;若中途失败,单例仍需先发布给 lifespan 清理入口。
_retain_failed_singleton = True
@@ -1148,7 +1176,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
:param stop_event: 当前 worker 代专属停止信号热更新后不会被重新清除
"""
while not global_vars.is_system_stopped and not stop_event.is_set():
while not runtime_stop_state.is_system_stopped and not stop_event.is_set():
try:
item: TransferQueue = self._queue.get(
block=True, timeout=self._transfer_interval
@@ -1156,10 +1184,10 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
if item is self._QUEUE_STOP_SENTINEL:
self._queue.task_done()
self.__settle_transfer_progress_if_idle()
if stop_event.is_set() or global_vars.is_system_stopped:
if stop_event.is_set() or runtime_stop_state.is_system_stopped:
break
continue
if stop_event.is_set() or global_vars.is_system_stopped:
if stop_event.is_set() or runtime_stop_state.is_system_stopped:
# 关闭信号与 queue.get 竞态时,把尚未处理的任务放回队列;其
# TransferPending 登记保持不变,供同进程重启 worker 或下次启动回放。
self._queue.put(item)
@@ -1606,7 +1634,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
try:
total_num = len(torrents)
for index, torrent in enumerate(torrents, start=1):
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if progress_callback:
torrent_name = (
@@ -1732,7 +1760,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
`predicate` `None`则默认保留所有项
:param verify_file_exists: 验证目录或文件是否存在默认值为 `True`
"""
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
raise OperationInterrupted()
storagechain = StorageChain()
@@ -2536,7 +2564,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
skipped_torrents = set()
try:
for file_item, bluray_dir in file_items:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
raise OperationInterrupted()
if continue_callback and not continue_callback():
raise OperationInterrupted()
@@ -2762,7 +2790,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
progress.update(value=0, text=__process_msg)
try:
for transfer_task in transfer_tasks:
if global_vars.is_system_stopped:
if runtime_stop_state.is_system_stopped:
break
if continue_callback and not continue_callback():
break
+5 -6
View File
@@ -2,14 +2,13 @@ import secrets
from dataclasses import dataclass
from typing import Any, Literal, Optional, Tuple, Union
from app.chain import ChainBase
from app.application.security.token import get_password_hash, verify_password
from app.application.chain.data import get_chain_user_port
from app.runtime.log import logger
from app.schemas.event import AuthCredentials
from app.schemas.event import AuthInterceptCredentials
from app.schemas.types import ChainEventType
from app.application.security.otp import OtpUtils
from app.application.security.token import get_password_hash, verify_password
from app.chain import ChainBase
from app.runtime.log import logger
from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import ChainEventType
PASSWORD_INVALID_CREDENTIALS_MESSAGE = "用户名、密码或验证码错误"
User = Any
+8 -12
View File
@@ -13,19 +13,15 @@ from typing import Any, Callable, List, Optional, Tuple
from pydantic import BaseModel
from app.chain import ChainBase
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.application.workflow import get_workflow_manager
from app.application.chain.data import get_chain_workflow_port
from app.application.workflow import get_workflow_manager
from app.chain import ChainBase
from app.runtime.events import Event, eventmanager
from app.runtime.execution import OwnedThreadPoolExecutor
from app.runtime.log import logger
from app.schemas.workflow import ActionContext
from app.schemas.workflow import ActionFlow
from app.schemas.workflow import Action
from app.schemas.workflow import ActionExecution
from app.schemas.workflow import ActionResult
from app.runtime.stop import runtime_stop_state
from app.schemas.types import EventType
from app.schemas.workflow import Action, ActionContext, ActionExecution, ActionFlow, ActionResult
ARTIFACT_FIELDS = {"torrents", "medias", "fileitems", "downloads", "sites", "subscribes"}
DEFAULT_WORKFLOW_MAX_WORKERS = 4
@@ -113,7 +109,7 @@ class WorkflowCancelToken:
"""
return bool(
(self.stop_event and self.stop_event.is_set())
or global_vars.is_workflow_stopped(self.workflow_id)
or runtime_stop_state.is_workflow_stopped(self.workflow_id)
)
@@ -236,7 +232,7 @@ class WorkflowExecutor:
self._registered_execution = callable(register)
self._admission_state = "admitted"
# 只有获得执行准入后才能清除历史单工作流停止标记。
global_vars.workflow_resume(self.workflow.id)
runtime_stop_state.resume_workflow(self.workflow.id)
return True
def request_stop(self) -> None:
@@ -285,7 +281,7 @@ class WorkflowExecutor:
"""判断本次执行或全局工作流是否已收到停止请求。"""
return bool(
self._stop_event.is_set()
or global_vars.is_workflow_stopped(self.workflow.id)
or runtime_stop_state.is_workflow_stopped(self.workflow.id)
)
def get_workflow_max_workers(self) -> int: