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
+4 -1
View File
@@ -65,6 +65,9 @@ jobs:
- name: Check process runtime service locators - name: Check process runtime service locators
run: uv run --locked --no-sync python scripts/architecture/service_locator.py run: uv run --locked --no-sync python scripts/architecture/service_locator.py
- name: Check Ruff diagnostic ratchet
run: uv run --locked --no-sync python scripts/architecture/ruff_ratchet.py
- name: Check mypy error ratchet - name: Check mypy error ratchet
run: uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py run: uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py
@@ -108,7 +111,6 @@ jobs:
run: uv run --locked --no-sync python tests/run.py --shard "${{ matrix.shard }}" run: uv run --locked --no-sync python tests/run.py --shard "${{ matrix.shard }}"
coverage: coverage:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Coverage Report name: Coverage Report
timeout-minutes: 20 timeout-minutes: 20
@@ -138,6 +140,7 @@ jobs:
uv run --locked --no-sync python -m coverage report uv run --locked --no-sync python -m coverage report
uv run --locked --no-sync python -m coverage json uv run --locked --no-sync python -m coverage json
uv run --locked --no-sync python -m coverage xml uv run --locked --no-sync python -m coverage xml
uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py
- name: Upload coverage report - name: Upload coverage report
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
+3 -3
View File
@@ -5,24 +5,24 @@ from typing import cast
from fastapi import Depends, Request from fastapi import Depends, Request
from app.application.messaging.chat import AsyncAgentChatRepository, AsyncUnitOfWork
from app.application.outbox import AsyncOutboxTransaction
from app.application.configuration import ( from app.application.configuration import (
ApiRuntimeConfig, ApiRuntimeConfig,
get_api_runtime_config_snapshot, get_api_runtime_config_snapshot,
) )
from app.application.messaging.chat import AsyncAgentChatRepository, AsyncUnitOfWork
from app.application.outbox import AsyncOutboxTransaction
from app.application.subscription.delete import SubscribeDeletionRepository from app.application.subscription.delete import SubscribeDeletionRepository
from app.application.subscription.identity import SubscribeIdentityDeletionRepository from app.application.subscription.identity import SubscribeIdentityDeletionRepository
from app.application.subscription.mutation import ( from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository, SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository, SubscriptionMutationRepository,
) )
from app.runtime.tasks import TaskRegistry, get_task_registry
from app.startup.composition.context import ( from app.startup.composition.context import (
AgentChatRuntime, AgentChatRuntime,
HostRuntime, HostRuntime,
SubscriptionRuntime, SubscriptionRuntime,
) )
from app.runtime.tasks import TaskRegistry, get_task_registry
def get_host_runtime(request: Request) -> HostRuntime: def get_host_runtime(request: Request) -> HostRuntime:
-1
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
from collections.abc import AsyncGenerator, Callable, Generator from collections.abc import AsyncGenerator, Callable, Generator
from typing import Any from typing import Any
SessionProvider = Callable[[], Generator[Any, None, None]] SessionProvider = Callable[[], Generator[Any, None, None]]
AsyncSessionProvider = Callable[[], AsyncGenerator[Any, None]] AsyncSessionProvider = Callable[[], AsyncGenerator[Any, None]]
RepositoryFactory = Callable[[Any], Any] RepositoryFactory = Callable[[Any], Any]
+2 -2
View File
@@ -4,15 +4,15 @@ from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.context import ( from app.api.context import (
get_agent_chat_runtime,
get_agent_chat_repository, get_agent_chat_repository,
get_agent_chat_runtime,
get_agent_chat_transaction, get_agent_chat_transaction,
get_async_session, get_async_session,
get_host_runtime, get_host_runtime,
) )
from app.application.messaging.chat import ( from app.application.messaging.chat import (
AgentChatService,
AgentChatPersistenceService, AgentChatPersistenceService,
AgentChatService,
AsyncAgentChatRepository, AsyncAgentChatRepository,
AsyncUnitOfWork, AsyncUnitOfWork,
) )
+11 -4
View File
@@ -9,25 +9,33 @@ from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.api.context import ( from app.api.context import (
get_async_session, get_async_session,
get_background_task_registry,
get_host_runtime, get_host_runtime,
get_subscription_history_repository, get_subscription_history_repository,
get_subscription_outbox, get_subscription_outbox,
get_subscription_repository, get_subscription_repository,
get_subscription_transaction, get_subscription_transaction,
get_sync_session, get_sync_session,
resolve_background_task_registry,
) )
from app.application.outbox import AsyncOutboxTransaction from app.application.outbox import AsyncOutboxTransaction
from app.application.scheduling import start_scheduler_job from app.application.scheduling import start_scheduler_job
from app.application.servarr import ServarrSubscriptionService from app.application.servarr import ServarrSubscriptionService
from app.application.subscription.delete import ( from app.application.subscription.delete import (
AsyncUnitOfWork as DeleteUnitOfWork, AsyncUnitOfWork as DeleteUnitOfWork,
)
from app.application.subscription.delete import (
DeleteSubscribeCommand, DeleteSubscribeCommand,
SubscribeDeletionRepository, SubscribeDeletionRepository,
) )
from app.application.subscription.identity import DeleteSubscriptionsByIdentityCommand from app.application.subscription.identity import (
from app.application.subscription.identity import SubscribeIdentityDeletionRepository DeleteSubscriptionsByIdentityCommand,
SubscribeIdentityDeletionRepository,
)
from app.application.subscription.mutation import ( from app.application.subscription.mutation import (
AsyncUnitOfWork as MutationUnitOfWork, AsyncUnitOfWork as MutationUnitOfWork,
)
from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository, SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository, SubscriptionMutationRepository,
SubscriptionMutationService, SubscriptionMutationService,
@@ -36,10 +44,9 @@ from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.search import SearchSubscriptionsCommand from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager from app.runtime.events import eventmanager
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.tasks import TaskRegistry
from app.schemas.types import EventType from app.schemas.types import EventType
from app.startup.composition.context import HostRuntime from app.startup.composition.context import HostRuntime
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.runtime.tasks import TaskRegistry
async def _publish_subscribe_deleted( async def _publish_subscribe_deleted(
+3 -3
View File
@@ -16,7 +16,7 @@ from app.application.workflow import (
WorkflowQueryService, WorkflowQueryService,
get_workflow_manager, get_workflow_manager,
) )
from app.runtime.config import global_vars from app.runtime.stop import runtime_stop_state
from app.startup.composition.context import HostRuntime from app.startup.composition.context import HostRuntime
@@ -36,7 +36,7 @@ def get_workflow_mutation_command(
load_event=workflow_manager.load_workflow_events, load_event=workflow_manager.load_workflow_events,
remove_event=workflow_manager.remove_workflow_event, remove_event=workflow_manager.remove_workflow_event,
refresh_event=workflow_manager.update_workflow_event, refresh_event=workflow_manager.update_workflow_event,
stop_running=global_vars.stop_workflow, stop_running=runtime_stop_state.stop_workflow,
delete_cache=lambda workflow_id: system_config.delete( delete_cache=lambda workflow_id: system_config.delete(
f"WorkflowCache-{workflow_id}" f"WorkflowCache-{workflow_id}"
), ),
@@ -52,7 +52,7 @@ def get_workflow_definition_command(
return WorkflowDefinitionCommand( return WorkflowDefinitionCommand(
repository=runtime.workflow.repository(db), repository=runtime.workflow.repository(db),
unit_of_work=runtime.persistence.async_transaction(db), unit_of_work=runtime.persistence.async_transaction(db),
stop_running=global_vars.stop_workflow, stop_running=runtime_stop_state.stop_workflow,
async_delete_cache=lambda workflow_id: system_config.async_delete( async_delete_cache=lambda workflow_id: system_config.async_delete(
f"WorkflowCache-{workflow_id}" f"WorkflowCache-{workflow_id}"
), ),
+47 -49
View File
@@ -7,8 +7,8 @@ import shutil
import time import time
import uuid import uuid
from collections import deque from collections import deque
from queue import Empty, Queue
from pathlib import Path from pathlib import Path
from queue import Empty, Queue
from threading import Lock from threading import Lock
from typing import Any, AsyncIterator, Awaitable, Callable, Optional, Union from typing import Any, AsyncIterator, Awaitable, Callable, Optional, Union
@@ -16,15 +16,58 @@ import aiofiles
from fastapi import Depends, File, Form, HTTPException, Request, UploadFile, status from fastapi import Depends, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from app.agent.contracts import ReplyMode, build_display_message
from app.agent.mcp import agent_mcp_manager
from app.agent.runtime_loader import get_moviepilot_agent_type
from app.api.dependencies.agent import (
get_agent_chat_persistence,
get_agent_chat_service,
)
from app.api.dependencies.auth import get_current_active_user
from app.api.presentation.sse import build_sse_error_response, build_sse_response
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.agent import (
get_running_agent_manager,
is_audio_input_available,
transcribe_audio,
)
from app.application.commands import get_command, get_commands
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.messaging.agent import (
agent_interaction_manager,
attach_web_agent_edit_queue,
attach_web_agent_message_queue,
build_agent_choice_button_rows,
create_web_agent_background_task,
detach_web_agent_edit_queue,
detach_web_agent_message_queue,
is_web_agent_message_for_user,
normalize_web_agent_button_rows,
parse_agent_choice_callback,
)
from app.application.messaging.chat import (
AgentChatPersistenceService,
AgentChatRecord,
AgentChatService,
get_configured_agent_chat_persistence,
get_configured_agent_chat_service,
)
from app.application.messaging.router import has_pending_interaction
from app.application.security.user import get_configured_user_id_lookup
from app.chain.message import MessageChain
from app.runtime.execution import run_in_threadpool from app.runtime.execution import run_in_threadpool
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.agent import AgentChatDisplaySaveRequest as _SchemaAgentChatDisplaySaveRequest from app.schemas.agent import AgentChatDisplaySaveRequest as _SchemaAgentChatDisplaySaveRequest
from app.schemas.agent import AgentChatSessionDetail as _SchemaAgentChatSessionDetail from app.schemas.agent import AgentChatSessionDetail as _SchemaAgentChatSessionDetail
from app.schemas.agent import AgentChatSessionSummary as _SchemaAgentChatSessionSummary from app.schemas.agent import AgentChatSessionSummary as _SchemaAgentChatSessionSummary
from app.schemas.agent import AgentChatUploadAttachment as _SchemaAgentChatUploadAttachment from app.schemas.agent import AgentChatUploadAttachment as _SchemaAgentChatUploadAttachment
from app.schemas.agent import AgentMcpServerListData as _SchemaAgentMcpServerListData from app.schemas.agent import AgentMcpServerListData as _SchemaAgentMcpServerListData
from app.schemas.agent import AgentMcpServersSaveRequest as _SchemaAgentMcpServersSaveRequest
from app.schemas.agent import AgentMcpServerTestRequest as _SchemaAgentMcpServerTestRequest from app.schemas.agent import AgentMcpServerTestRequest as _SchemaAgentMcpServerTestRequest
from app.schemas.agent import AgentMcpServerTestResult as _SchemaAgentMcpServerTestResult from app.schemas.agent import AgentMcpServerTestResult as _SchemaAgentMcpServerTestResult
from app.schemas.agent import AgentMcpServersSaveRequest as _SchemaAgentMcpServersSaveRequest
from app.schemas.agent import AgentSessionStopData as _SchemaAgentSessionStopData from app.schemas.agent import AgentSessionStopData as _SchemaAgentSessionStopData
from app.schemas.agent import AgentWebCallbackData as _SchemaAgentWebCallbackData from app.schemas.agent import AgentWebCallbackData as _SchemaAgentWebCallbackData
from app.schemas.agent import AgentWebCommandInfo as _SchemaAgentWebCommandInfo from app.schemas.agent import AgentWebCommandInfo as _SchemaAgentWebCommandInfo
@@ -32,52 +75,7 @@ from app.schemas.message import AgentWebChatRequest as _SchemaAgentWebChatReques
from app.schemas.message import AgentWebChoiceRequest as _SchemaAgentWebChoiceRequest from app.schemas.message import AgentWebChoiceRequest as _SchemaAgentWebChoiceRequest
from app.schemas.message import Message as _SchemaMessage from app.schemas.message import Message as _SchemaMessage
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter from app.schemas.types import NotificationChannel
from app.api.presentation.sse import build_sse_error_response, build_sse_response
from app.agent.contracts import ReplyMode, build_display_message
from app.agent.mcp import agent_mcp_manager
from app.agent.runtime_loader import get_moviepilot_agent_type
from app.application.agent import (
get_running_agent_manager,
is_audio_input_available,
transcribe_audio,
)
from app.chain.message import MessageChain
from app.application.commands import get_command, get_commands
from app.runtime.config import global_vars
from app.api.principal import ApiPrincipal
from app.api.dependencies.agent import (
get_agent_chat_persistence,
get_agent_chat_service,
)
from app.api.dependencies.auth import get_current_active_user
from app.application.messaging.chat import (
AgentChatRecord,
AgentChatPersistenceService,
AgentChatService,
get_configured_agent_chat_service,
get_configured_agent_chat_persistence,
)
from app.application.security.user import get_configured_user_id_lookup
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.messaging.agent import (
attach_web_agent_message_queue,
attach_web_agent_edit_queue,
create_web_agent_background_task,
detach_web_agent_message_queue,
detach_web_agent_edit_queue,
is_web_agent_message_for_user,
)
from app.application.messaging.agent import agent_interaction_manager
from app.application.messaging.agent import (
build_agent_choice_button_rows,
normalize_web_agent_button_rows,
parse_agent_choice_callback,
)
from app.application.messaging.router import has_pending_interaction
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.schemas.types import EventType, NotificationChannel
router = ResponseAPIRouter() router = ResponseAPIRouter()
@@ -2274,7 +2272,7 @@ async def _web_agent_stream_impl(
{"session_id": session_id}, {"session_id": session_id},
locale=locale, locale=locale,
) )
while not global_vars.is_system_stopped: while not runtime_stop_state.is_system_stopped:
if await request.is_disconnected(): if await request.is_disconnected():
disconnected = True disconnected = True
break break
+4 -4
View File
@@ -2,13 +2,13 @@ from typing import Annotated, Optional
from fastapi import Depends, Query from fastapi import Depends, Query
from app.schemas.context import MediaPerson as _SchemaMediaPerson from app.adapters.web.security.access import verify_token
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.anilist import AniListChain from app.chain.anilist import AniListChain
from app.domain.context import MediaInfo from app.domain.context import MediaInfo
from app.adapters.web.security.access import verify_token from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+11 -11
View File
@@ -5,15 +5,15 @@ from typing import AsyncIterator, List, Optional
from fastapi import APIRouter, Depends, Header, Security from fastapi import APIRouter, Depends, Header, Security
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.schemas.openai import AnthropicErrorDetail as _SchemaAnthropicErrorDetail from app.adapters.web.security.access import anthropic_api_key_header
from app.schemas.openai import AnthropicErrorResponse as _SchemaAnthropicErrorResponse from app.api.context import (
from app.schemas.openai import AnthropicMessagesRequest as _SchemaAnthropicMessagesRequest get_background_task_registry_compat,
from app.schemas.openai import AnthropicMessagesResponse as _SchemaAnthropicMessagesResponse resolve_background_task_registry,
from app.schemas.openai import AnthropicTextBlock as _SchemaAnthropicTextBlock )
from app.api.endpoints.openai import ( from app.api.endpoints.openai import (
MODEL_ID, MODEL_ID,
_is_manager_unavailable,
_is_manager_queue_full, _is_manager_queue_full,
_is_manager_unavailable,
_run_managed_agent, _run_managed_agent,
) )
from app.api.openai_utils import ( from app.api.openai_utils import (
@@ -24,12 +24,12 @@ from app.api.openai_utils import (
from app.api.presentation.sse import build_sse_response, encode_named_event from app.api.presentation.sse import build_sse_response, encode_named_event
from app.application.agent import get_running_agent_manager from app.application.agent import get_running_agent_manager
from app.application.configuration import get_api_runtime_config_snapshot from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import anthropic_api_key_header
from app.api.context import (
get_background_task_registry_compat,
resolve_background_task_registry,
)
from app.runtime.tasks import TaskRegistry from app.runtime.tasks import TaskRegistry
from app.schemas.openai import AnthropicErrorDetail as _SchemaAnthropicErrorDetail
from app.schemas.openai import AnthropicErrorResponse as _SchemaAnthropicErrorResponse
from app.schemas.openai import AnthropicMessagesRequest as _SchemaAnthropicMessagesRequest
from app.schemas.openai import AnthropicMessagesResponse as _SchemaAnthropicMessagesResponse
from app.schemas.openai import AnthropicTextBlock as _SchemaAnthropicTextBlock
ANTHROPIC_ERROR_RESPONSES = { ANTHROPIC_ERROR_RESPONSES = {
400: {"model": _SchemaAnthropicErrorResponse, "description": "请求格式错误"}, 400: {"model": _SchemaAnthropicErrorResponse, "description": "请求格式错误"},
+4 -4
View File
@@ -3,12 +3,12 @@ from typing import Any
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from app.api.dependencies.auth import get_auth_service
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.plugin.runtime import get_plugin_manager
from app.application.security.auth import AuthService, consume_plugin_auth_ticket
from app.schemas.token import Token as _SchemaToken from app.schemas.token import Token as _SchemaToken
from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.security.auth import AuthService, consume_plugin_auth_ticket
from app.application.plugin.runtime import get_plugin_manager
from app.api.dependencies.auth import get_auth_service
router = ResponseAPIRouter() router = ResponseAPIRouter()
+5 -5
View File
@@ -1,14 +1,14 @@
from typing import List, Any, Optional from typing import Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.context import MediaPerson as _SchemaMediaPerson from app.adapters.web.security.access import verify_token
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.bangumi import BangumiChain from app.chain.bangumi import BangumiChain
from app.domain.context import MediaInfo from app.domain.context import MediaInfo
from app.adapters.web.security.access import verify_token from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+13 -13
View File
@@ -1,8 +1,20 @@
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional, Annotated from typing import Annotated, Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.adapters.system.host import SystemUtils
from app.adapters.web.security.access import verify_apitoken
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
from app.api.dependencies.auth import get_current_active_superuser
from app.api.dependencies.history import get_dashboard_query_service
from app.api.response import ResponseAPIRouter
from app.application.configuration import ApiRuntimeConfig
from app.application.dashboard import DashboardQueryService
from app.application.directory import DirectoryHelper
from app.application.scheduling import get_scheduler
from app.chain.dashboard import DashboardChain
from app.chain.storage import StorageChain
from app.runtime.execution import run_in_threadpool from app.runtime.execution import run_in_threadpool
from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryInfo from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryInfo
from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo
@@ -13,19 +25,7 @@ from app.schemas.dashboard import ScheduleProgress as _SchemaScheduleProgress
from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.schemas.dashboard import Storage as _SchemaStorage from app.schemas.dashboard import Storage as _SchemaStorage
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.dashboard import DashboardChain
from app.chain.storage import StorageChain
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
from app.application.configuration import ApiRuntimeConfig
from app.adapters.web.security.access import verify_apitoken
from app.api.dependencies.auth import get_current_active_superuser
from app.api.dependencies.history import get_dashboard_query_service
from app.application.dashboard import DashboardQueryService
from app.schemas.types import StorageAction from app.schemas.types import StorageAction
from app.application.directory import DirectoryHelper
from app.application.scheduling import get_scheduler
from app.adapters.system.host import SystemUtils
router = ResponseAPIRouter() router = ResponseAPIRouter()
+4 -4
View File
@@ -2,17 +2,17 @@ from typing import Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.event import DiscoverMediaSource as _SchemaDiscoverMediaSource from app.adapters.web.security.access import verify_token
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.bangumi import BangumiChain from app.chain.bangumi import BangumiChain
from app.chain.douban import DoubanChain from app.chain.douban import DoubanChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
from app.runtime.events import eventmanager from app.runtime.events import eventmanager
from app.adapters.web.security.access import verify_token from app.schemas.event import DiscoverMediaSource as _SchemaDiscoverMediaSource
from app.schemas.event import DiscoverSourceEventData from app.schemas.event import DiscoverSourceEventData
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.types import ChainEventType, MediaType from app.schemas.types import ChainEventType, MediaType
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+4 -4
View File
@@ -2,14 +2,14 @@ from typing import Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.context import MediaPerson as _SchemaMediaPerson from app.adapters.web.security.access import verify_token
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.douban import DoubanChain from app.chain.douban import DoubanChain
from app.domain.context import MediaInfo from app.domain.context import MediaInfo
from app.adapters.web.security.access import verify_token from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+22 -22
View File
@@ -1,7 +1,26 @@
from typing import Any, List, Annotated, Optional, Union from typing import Annotated, Any, List, Optional, Union
from fastapi import Depends, Body from fastapi import Body, Depends
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import get_current_active_user
from app.api.dependencies.site import get_site_sync_query_service
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_configured_system_config
from app.application.directory import DirectoryHelper
from app.application.security.url import SecurityUtils
from app.application.site.query import (
SiteQueryService,
get_configured_site_query_service,
)
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
from app.domain.media import is_music_media_source, normalize_music_type
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo
from app.schemas.download import DownloadAddedData as _SchemaDownloadAddedData from app.schemas.download import DownloadAddedData as _SchemaDownloadAddedData
from app.schemas.download import DownloadDirectory as _SchemaDownloadDirectory from app.schemas.download import DownloadDirectory as _SchemaDownloadDirectory
@@ -13,24 +32,6 @@ from app.schemas.system import TorrentInfo as _SchemaTorrentInfo
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.transfer import DownloaderTorrent as _SchemaDownloaderTorrent from app.schemas.transfer import DownloaderTorrent as _SchemaDownloaderTorrent
from app.schemas.transfer import MusicInfo as _SchemaMusicInfo from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
from app.adapters.web.security.access import verify_token
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.application.site.query import (
SiteQueryService,
get_configured_site_query_service,
)
from app.api.dependencies.auth import get_current_active_user
from app.api.dependencies.site import get_site_sync_query_service
from app.application.directory import DirectoryHelper
from app.schemas.types import ( from app.schemas.types import (
MUSIC_ENTITY_RECORDING, MUSIC_ENTITY_RECORDING,
MediaSource, MediaSource,
@@ -38,8 +39,7 @@ from app.schemas.types import (
MusicTargetEntityType, MusicTargetEntityType,
SystemConfigKey, SystemConfigKey,
) )
from app.domain.media import is_music_media_source, normalize_music_type from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.application.security.url import SecurityUtils
router = ResponseAPIRouter() router = ResponseAPIRouter()
+15 -15
View File
@@ -1,33 +1,21 @@
import time import time
from collections.abc import Coroutine from collections.abc import Coroutine
from typing import List, Any, Callable, Optional from typing import Any, Callable, List, Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.common import BatchProgressKeyData as _SchemaBatchProgressKeyData from app.adapters.web.security.access import verify_token
from app.schemas.common import ProgressKeyData as _SchemaProgressKeyData
from app.schemas.history import BatchTransferHistoryRedoRequest as _SchemaBatchTransferHistoryRedoRequest
from app.schemas.history import TransferHistory as _SchemaTransferHistory
from app.schemas.history import TransferHistoryPage as _SchemaTransferHistoryPage
from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.history import DownloadHistory as _SchemaDownloadHistory
from app.api.response import ResponseAPIRouter
from app.agent.contracts import ReplyMode from app.agent.contracts import ReplyMode
from app.application.agent import get_running_agent_manager
from app.agent.prompt.transfer_redo import ( from app.agent.prompt.transfer_redo import (
build_batch_manual_redo_prompt, build_batch_manual_redo_prompt,
build_manual_redo_prompt, build_manual_redo_prompt,
) )
from app.runtime.config import global_vars
from app.api.context import ( from app.api.context import (
get_api_runtime_config, get_api_runtime_config,
get_background_task_registry, get_background_task_registry,
resolve_api_runtime_config, resolve_api_runtime_config,
resolve_background_task_registry, resolve_background_task_registry,
) )
from app.application.configuration import ApiRuntimeConfig
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_superuser, get_current_active_superuser,
@@ -37,14 +25,26 @@ from app.api.dependencies.history import (
get_history_query_service, get_history_query_service,
get_transfer_history_mutation_command, get_transfer_history_mutation_command,
) )
from app.runtime.progress import AsyncProgressHelper from app.api.response import ResponseAPIRouter
from app.application.agent import get_running_agent_manager
from app.application.configuration import ApiRuntimeConfig
from app.application.history import ( from app.application.history import (
DownloadHistoryMutationCommand, DownloadHistoryMutationCommand,
HistoryQueryService, HistoryQueryService,
TransferHistoryMutationCommand, TransferHistoryMutationCommand,
) )
from app.runtime.config import global_vars
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.progress import AsyncProgressHelper
from app.runtime.tasks import TaskRegistry from app.runtime.tasks import TaskRegistry
from app.schemas.common import BatchProgressKeyData as _SchemaBatchProgressKeyData
from app.schemas.common import ProgressKeyData as _SchemaProgressKeyData
from app.schemas.history import BatchTransferHistoryRedoRequest as _SchemaBatchTransferHistoryRedoRequest
from app.schemas.history import DownloadHistory as _SchemaDownloadHistory
from app.schemas.history import TransferHistory as _SchemaTransferHistory
from app.schemas.history import TransferHistoryPage as _SchemaTransferHistoryPage
from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload
router = ResponseAPIRouter() router = ResponseAPIRouter()
+3 -3
View File
@@ -3,11 +3,11 @@ from typing import Any, Dict, List, Optional, Union
from fastapi import Depends, Request, Response from fastapi import Depends, Request, Response
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from app.agent.llm.gateway import resolve_llm_provider_runtime
from app.api.dependencies.auth import get_current_active_superuser_async
from app.api.response import ResponseAPIRouter
from app.schemas.common import ManageRequest as _SchemaManageRequest from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.api.dependencies.auth import get_current_active_superuser_async
from app.agent.llm.gateway import resolve_llm_provider_runtime
router = ResponseAPIRouter() router = ResponseAPIRouter()
+10 -10
View File
@@ -1,22 +1,22 @@
from datetime import timedelta from datetime import timedelta
from typing import Any, List, Annotated from typing import Annotated, Any, List
from fastapi import Depends, Form, HTTPException, Request, Response from fastapi import Depends, Form, HTTPException, Request, Response
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.configuration import ApiRuntimeConfig, get_configured_system_config
from app.application.image import WallpaperHelper
from app.application.security.token import create_access_token
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain.user import MfaRequired, UserChain
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import MfaChallenge as _SchemaMfaChallenge from app.schemas.token import MfaChallenge as _SchemaMfaChallenge
from app.schemas.token import Token as _SchemaToken from app.schemas.token import Token as _SchemaToken
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.chain.user import MfaRequired, UserChain
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.application.security.token import create_access_token
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
from app.application.configuration import ApiRuntimeConfig, get_configured_system_config
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.application.image import WallpaperHelper
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
router = ResponseAPIRouter() router = ResponseAPIRouter()
+6 -6
View File
@@ -1,8 +1,13 @@
from typing import List, Any, Dict, Annotated, Union from typing import Annotated, Any, Dict, List, Union
from fastapi import Depends, HTTPException, Request from fastapi import Depends, HTTPException, Request
from fastapi.responses import JSONResponse, Response from fastapi.responses import JSONResponse, Response
from app.adapters.web.security.access import verify_apikey
from app.agent.tools.manager import moviepilot_tool_manager
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.runtime.log import logger
from app.runtime.version import get_app_version
from app.schemas.mcp import MCP_JSONRPC_REQUEST_SCHEMA as _SchemaMCP_JSONRPC_REQUEST_SCHEMA from app.schemas.mcp import MCP_JSONRPC_REQUEST_SCHEMA as _SchemaMCP_JSONRPC_REQUEST_SCHEMA
from app.schemas.mcp import McpJsonRpcError as _SchemaMcpJsonRpcError from app.schemas.mcp import McpJsonRpcError as _SchemaMcpJsonRpcError
from app.schemas.mcp import McpJsonRpcResponse as _SchemaMcpJsonRpcResponse from app.schemas.mcp import McpJsonRpcResponse as _SchemaMcpJsonRpcResponse
@@ -11,11 +16,6 @@ from app.schemas.mcp import McpToolInfo as _SchemaMcpToolInfo
from app.schemas.mcp import ToolCallData as _SchemaToolCallData from app.schemas.mcp import ToolCallData as _SchemaToolCallData
from app.schemas.mcp import ToolCallRequest as _SchemaToolCallRequest from app.schemas.mcp import ToolCallRequest as _SchemaToolCallRequest
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.agent.tools.manager import moviepilot_tool_manager
from app.adapters.web.security.access import verify_apikey
from app.runtime.version import get_app_version
from app.runtime.log import logger
router = ResponseAPIRouter() router = ResponseAPIRouter()
+19 -19
View File
@@ -5,36 +5,36 @@ from uuid import UUID
from fastapi import Depends, Query from fastapi import Depends, Query
from pydantic import BeforeValidator from pydantic import BeforeValidator
from app.adapters.web.security.access import verify_apitoken, verify_token
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_user,
)
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_api_runtime_config_snapshot
from app.chain.media import MediaChain
from app.chain.scraping import ScrapingChain
from app.chain.tmdb import TmdbChain
from app.domain.context import Context, MusicInfo
from app.domain.media import is_music_media_source, normalize_music_type, parse_media_source_selection
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.schemas.category import CategoryConfig
from app.schemas.category import CategoryConfig as _SchemaCategoryConfig from app.schemas.category import CategoryConfig as _SchemaCategoryConfig
from app.schemas.category import MediaCategoryMap as _SchemaMediaCategoryMap from app.schemas.category import MediaCategoryMap as _SchemaMediaCategoryMap
from app.schemas.context import MediaEpisodeGroup as _SchemaMediaEpisodeGroup from app.schemas.context import MediaEpisodeGroup as _SchemaMediaEpisodeGroup
from app.schemas.context import MediaPerson as _SchemaMediaPerson from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.context import MediaSearchResults as _SchemaMediaSearchResults from app.schemas.context import MediaSearchResults as _SchemaMediaSearchResults
from app.schemas.context import MediaSeason as _SchemaMediaSeason from app.schemas.context import MediaSeason as _SchemaMediaSeason
from app.schemas.event import MediaSourceInfo as _SchemaMediaSourceInfo
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
from app.schemas.workflow import Context as _SchemaContext from app.schemas.workflow import Context as _SchemaContext
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.scraping import ScrapingChain
from app.chain.tmdb import TmdbChain
from app.application.configuration import get_api_runtime_config_snapshot
from app.domain.context import Context, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_user,
)
from app.schemas.category import CategoryConfig
from app.schemas.event import MediaSourceInfo as _SchemaMediaSourceInfo
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
from app.domain.media import is_music_media_source, normalize_music_type, parse_media_source_selection
from app.schemas.media import normalize_media_source, resolve_media_identity
router = ResponseAPIRouter() router = ResponseAPIRouter()
+15 -15
View File
@@ -2,32 +2,32 @@ from typing import Any, List, Optional
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from app.adapters.web.security.access import verify_token
from app.api.dependencies.history import get_mediaserver_query_service
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_configured_system_config
from app.application.mediaserver import (
MediaServerQueryService,
get_mediaserver_configs,
)
from app.chain.download import DownloadChain
from app.chain.mediaserver import MediaServerChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo
from app.schemas.media import build_media_key, resolve_media_identity
from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo
from app.schemas.mediaserver import MediaServerExistingEpisodes as _SchemaMediaServerExistingEpisodes from app.schemas.mediaserver import MediaServerExistingEpisodes as _SchemaMediaServerExistingEpisodes
from app.schemas.mediaserver import MediaServerExistsData as _SchemaMediaServerExistsData from app.schemas.mediaserver import MediaServerExistsData as _SchemaMediaServerExistsData
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayData as _SchemaMediaServerPlayData from app.schemas.mediaserver import MediaServerPlayData as _SchemaMediaServerPlayData
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.mediaserver import NotExistMediaInfo as _SchemaNotExistMediaInfo from app.schemas.mediaserver import NotExistMediaInfo as _SchemaNotExistMediaInfo
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.download import DownloadChain
from app.chain.mediaserver import MediaServerChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.adapters.web.security.access import verify_token
from app.application.configuration import get_configured_system_config
from app.application.mediaserver import (
MediaServerQueryService,
get_mediaserver_configs,
)
from app.api.dependencies.history import get_mediaserver_query_service
from app.schemas.mediaserver import NotExistMediaInfo
from app.schemas.types import MediaSource, MediaType, SystemConfigKey from app.schemas.types import MediaSource, MediaType, SystemConfigKey
from app.schemas.media import build_media_key, resolve_media_identity from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+17 -17
View File
@@ -7,6 +7,23 @@ from typing import Annotated, Any, List, Optional, Protocol, Union
from fastapi import Depends, Request from fastapi import Depends, Request
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
from app.adapters.external.wechat_crypt import WXBizMsgCrypt
from app.adapters.web.security.access import verify_apitoken, verify_token
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.api.dependencies.agent import get_message_query_service
from app.api.dependencies.auth import get_current_active_superuser
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.configuration import (
get_api_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.messaging.message import MessageQueryService
from app.application.notification import get_notification_configs
from app.chain.message import MessageChain
from app.runtime.config import global_vars
from app.runtime.log import logger
from app.runtime.tasks import TaskRegistry
from app.schemas.message import MessageClearBefore as _SchemaMessageClearBefore from app.schemas.message import MessageClearBefore as _SchemaMessageClearBefore
from app.schemas.message import MessageClearData as _SchemaMessageClearData from app.schemas.message import MessageClearData as _SchemaMessageClearData
from app.schemas.message import MessageClearScope as _SchemaMessageClearScope from app.schemas.message import MessageClearScope as _SchemaMessageClearScope
@@ -16,24 +33,7 @@ from app.schemas.message import SubscriptionMessage as _SchemaSubscriptionMessag
from app.schemas.message import WebMessageItem as _SchemaWebMessageItem from app.schemas.message import WebMessageItem as _SchemaWebMessageItem
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import ResponseAPIRouter
from app.chain.message import MessageChain
from app.runtime.config import global_vars
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.principal import ApiPrincipal
from app.application.configuration import (
get_api_runtime_config_snapshot,
get_configured_system_config,
)
from app.api.dependencies.agent import get_message_query_service
from app.api.dependencies.auth import get_current_active_superuser
from app.application.messaging.message import MessageQueryService
from app.application.notification import get_notification_configs
from app.runtime.log import logger
from app.adapters.external.wechat_crypt import WXBizMsgCrypt
from app.schemas.types import NotificationChannel, SystemConfigKey from app.schemas.types import NotificationChannel, SystemConfigKey
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.runtime.tasks import TaskRegistry
router = ResponseAPIRouter() router = ResponseAPIRouter()
+29 -29
View File
@@ -4,10 +4,37 @@ MFA (Multi-Factor Authentication) API 端点
""" """
import json import json
from typing import Any, Annotated, Optional from typing import Annotated, Any, Optional
from fastapi import Depends, HTTPException, Body, Request, Response from fastapi import Body, Depends, HTTPException, Request, Response
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.api.dependencies.auth import (
get_current_active_user,
get_current_active_user_async,
get_passkey_service,
get_user_service,
)
from app.api.principal import ApiPrincipal
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.application.security.auth import get_configured_auth_service
from app.application.security.otp import OtpUtils
from app.application.security.passkey import (
PasskeyChallengeStore,
PassKeyHelper,
PassKeyRegistrationOriginMismatchError,
PassKeyRegistrationVerificationError,
)
from app.application.security.passkeys import (
PasskeyService,
)
from app.application.security.token import verify_password
from app.application.security.user import (
UserService,
get_configured_user_id_lookup,
get_configured_user_name_lookup,
)
from app.runtime.log import logger
from app.schemas.mcp import BaseModel as _SchemaBaseModel from app.schemas.mcp import BaseModel as _SchemaBaseModel
from app.schemas.mcp import JsonData as _SchemaJsonData from app.schemas.mcp import JsonData as _SchemaJsonData
from app.schemas.mfa import MfaStatusData as _SchemaMfaStatusData from app.schemas.mfa import MfaStatusData as _SchemaMfaStatusData
@@ -17,33 +44,6 @@ from app.schemas.mfa import PasskeyStartData as _SchemaPasskeyStartData
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import Token as _SchemaToken from app.schemas.token import Token as _SchemaToken
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
from app.application.security.token import verify_password
from app.application.security.auth import get_configured_auth_service
from app.application.security.user import UserService
from app.application.security.user import (
get_configured_user_id_lookup,
get_configured_user_name_lookup,
)
from app.application.security.passkeys import (
PasskeyService,
)
from app.api.principal import ApiPrincipal
from app.api.dependencies.auth import (
get_current_active_user,
get_current_active_user_async,
get_user_service,
get_passkey_service,
)
from app.application.security.passkey import (
PassKeyHelper,
PassKeyRegistrationOriginMismatchError,
PassKeyRegistrationVerificationError,
PasskeyChallengeStore,
)
from app.runtime.log import logger
from app.application.security.otp import OtpUtils
router = ResponseAPIRouter() router = ResponseAPIRouter()
+12 -12
View File
@@ -2,6 +2,18 @@ from typing import Annotated, Optional
from fastapi import Depends, HTTPException, Query from fastapi import Depends, HTTPException, Query
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import get_current_active_superuser_async
from app.api.response import ResponseAPIRouter
from app.chain.listenbrainz import (
LISTENBRAINZ_CHART_RANGES,
LISTENBRAINZ_FRESH_MAX_DAYS,
LISTENBRAINZ_FRESH_SORTS,
)
from app.chain.media import MediaChain
from app.chain.musicbrainz import MusicBrainzChain
from app.chain.recommend import RecommendChain
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
from app.schemas.music import MusicAlbumInfo as _SchemaMusicAlbumInfo from app.schemas.music import MusicAlbumInfo as _SchemaMusicAlbumInfo
from app.schemas.music import MusicArtistInfo as _SchemaMusicArtistInfo from app.schemas.music import MusicArtistInfo as _SchemaMusicArtistInfo
from app.schemas.music import MusicRecognitionCacheData as _SchemaMusicRecognitionCacheData from app.schemas.music import MusicRecognitionCacheData as _SchemaMusicRecognitionCacheData
@@ -9,19 +21,7 @@ from app.schemas.music import MusicRecognizeRequest as _SchemaMusicRecognizeRequ
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.transfer import MusicInfo as _SchemaMusicInfo from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.recommend import RecommendChain
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import get_current_active_superuser_async
from app.chain.listenbrainz import (
LISTENBRAINZ_CHART_RANGES,
LISTENBRAINZ_FRESH_MAX_DAYS,
LISTENBRAINZ_FRESH_SORTS,
)
from app.chain.musicbrainz import MusicBrainzChain
router = ResponseAPIRouter() router = ResponseAPIRouter()
+3 -3
View File
@@ -2,11 +2,11 @@ from typing import Any, Dict
from fastapi import Depends from fastapi import Depends
from app.schemas.common import ManageRequest as _SchemaManageRequest from app.api.dependencies.auth import get_current_active_superuser
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.notification import NotificationChain from app.chain.notification import NotificationChain
from app.api.dependencies.auth import get_current_active_superuser from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse
router = ResponseAPIRouter() router = ResponseAPIRouter()
+17 -17
View File
@@ -8,6 +8,23 @@ from fastapi import APIRouter, Depends, Request, Security
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security import HTTPAuthorizationCredentials from fastapi.security import HTTPAuthorizationCredentials
from app.adapters.web.security.access import openai_bearer_scheme
from app.agent.contracts import ReplyMode
from app.agent.runtime_loader import get_moviepilot_agent_type
from app.api.context import (
get_background_task_registry_compat,
resolve_background_task_registry,
)
from app.api.openai_utils import (
build_completion_payload,
build_prompt,
build_responses_input,
build_session_id,
)
from app.api.presentation.sse import build_sse_response, encode_data_event
from app.application.agent import get_running_agent_manager
from app.application.configuration import get_api_runtime_config_snapshot
from app.runtime.tasks import TaskRegistry
from app.schemas.openai import OpenAIChatCompletionResponse as _SchemaOpenAIChatCompletionResponse from app.schemas.openai import OpenAIChatCompletionResponse as _SchemaOpenAIChatCompletionResponse
from app.schemas.openai import OpenAIChatCompletionsRequest as _SchemaOpenAIChatCompletionsRequest from app.schemas.openai import OpenAIChatCompletionsRequest as _SchemaOpenAIChatCompletionsRequest
from app.schemas.openai import OpenAIErrorDetail as _SchemaOpenAIErrorDetail from app.schemas.openai import OpenAIErrorDetail as _SchemaOpenAIErrorDetail
@@ -19,24 +36,7 @@ from app.schemas.openai import OpenAIResponsesOutputText as _SchemaOpenAIRespons
from app.schemas.openai import OpenAIResponsesRequest as _SchemaOpenAIResponsesRequest from app.schemas.openai import OpenAIResponsesRequest as _SchemaOpenAIResponsesRequest
from app.schemas.openai import OpenAIResponsesResponse as _SchemaOpenAIResponsesResponse from app.schemas.openai import OpenAIResponsesResponse as _SchemaOpenAIResponsesResponse
from app.schemas.openai import OpenAIUsage as _SchemaOpenAIUsage from app.schemas.openai import OpenAIUsage as _SchemaOpenAIUsage
from app.api.openai_utils import (
build_completion_payload,
build_prompt,
build_responses_input,
build_session_id,
)
from app.api.presentation.sse import build_sse_response, encode_data_event
from app.agent.runtime_loader import get_moviepilot_agent_type
from app.application.agent import get_running_agent_manager
from app.agent.contracts import ReplyMode
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import openai_bearer_scheme
from app.schemas.types import NotificationChannel from app.schemas.types import NotificationChannel
from app.api.context import (
get_background_task_registry_compat,
resolve_background_task_registry,
)
from app.runtime.tasks import TaskRegistry
OPENAI_ERROR_RESPONSES = { OPENAI_ERROR_RESPONSES = {
400: {"model": _SchemaOpenAIErrorResponse, "description": "请求格式错误"}, 400: {"model": _SchemaOpenAIErrorResponse, "description": "请求格式错误"},
+38 -39
View File
@@ -9,11 +9,48 @@ from fastapi import Depends, Header, HTTPException, Security
from starlette import status from starlette import status
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from app.adapters.external.market import PluginHelper
from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.system.plugin.package import PluginPackageManager
from app.adapters.web.security.access import (
resource_token_cookie,
verify_resource_token,
verify_token,
)
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.api.dependencies.plugin import (
get_plugin_config_command,
)
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.commands import init_commands
from app.application.configuration import get_api_runtime_config_snapshot, get_configured_system_config
from app.application.plugin.config import PluginConfigCommand
from app.application.plugin.folders import remove_plugin_from_folders
from app.application.plugin.install import PluginInstallCommand
from app.application.plugin.routes import register_plugin_api, remove_plugin_api
from app.application.plugin.runtime import PluginRuntime, get_plugin_manager
from app.application.scheduling import remove_plugin_job, update_plugin_job
from app.runtime.cache import async_fresh
from app.runtime.execution import run_in_threadpool from app.runtime.execution import run_in_threadpool
from app.runtime.extensions.plugin.contracts import (
PluginDashboardError,
PluginNotFoundError,
)
from app.runtime.log import logger
from app.runtime.tasks import TaskRegistry
from app.schemas.common import JsonObject as _SchemaJsonObject from app.schemas.common import JsonObject as _SchemaJsonObject
from app.schemas.exception import (
PersistenceUnavailableError,
PluginMutationRejectedError,
)
from app.schemas.plugin import Plugin as _SchemaPlugin from app.schemas.plugin import Plugin as _SchemaPlugin
from app.schemas.plugin import PluginDashboard as _SchemaPluginDashboard
from app.schemas.plugin import PluginCloneRequest as _SchemaPluginCloneRequest from app.schemas.plugin import PluginCloneRequest as _SchemaPluginCloneRequest
from app.schemas.plugin import PluginDashboard as _SchemaPluginDashboard
from app.schemas.plugin import PluginDashboardMetaItem as _SchemaPluginDashboardMetaItem from app.schemas.plugin import PluginDashboardMetaItem as _SchemaPluginDashboardMetaItem
from app.schemas.plugin import PluginFoldersData as _SchemaPluginFoldersData from app.schemas.plugin import PluginFoldersData as _SchemaPluginFoldersData
from app.schemas.plugin import PluginRating as _SchemaPluginRating from app.schemas.plugin import PluginRating as _SchemaPluginRating
@@ -26,45 +63,7 @@ from app.schemas.plugin import PluginRuntimeSummary as _SchemaPluginRuntimeSumma
from app.schemas.plugin import PluginSidebarNavItem as _SchemaPluginSidebarNavItem from app.schemas.plugin import PluginSidebarNavItem as _SchemaPluginSidebarNavItem
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import ResponseAPIRouter
from app.application.plugin.folders import remove_plugin_from_folders
from app.application.plugin.routes import register_plugin_api, remove_plugin_api
from app.application.plugin.install import PluginInstallCommand
from app.application.plugin.config import PluginConfigCommand
from app.application.commands import init_commands
from app.application.scheduling import remove_plugin_job, update_plugin_job
from app.runtime.cache import async_fresh
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.plugin.runtime import PluginRuntime, get_plugin_manager
from app.runtime.extensions.plugin.contracts import (
PluginDashboardError,
PluginNotFoundError,
)
from app.adapters.web.security.access import (
resource_token_cookie,
verify_resource_token,
verify_token,
)
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.api.dependencies.plugin import (
get_plugin_config_command,
)
from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.external.market import PluginHelper
from app.adapters.system.plugin.package import PluginPackageManager
from app.schemas.exception import (
PersistenceUnavailableError,
PluginMutationRejectedError,
)
from app.runtime.log import logger
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.runtime.tasks import TaskRegistry
router = ResponseAPIRouter() router = ResponseAPIRouter()
_plugin_release_refresh_tasks: set[asyncio.Task] = set() _plugin_release_refresh_tasks: set[asyncio.Task] = set()
+6 -6
View File
@@ -2,17 +2,17 @@ from typing import Any, Awaitable, List, Optional
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from app.schemas.event import RecommendMediaSource as _SchemaRecommendMediaSource from app.adapters.web.security.access import verify_token
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter from app.api.response import ResponseAPIRouter
from app.chain.recommend import RecommendChain from app.chain.recommend import RecommendChain
from app.runtime.events import eventmanager from app.runtime.events import eventmanager
from app.adapters.web.security.access import verify_token from app.schemas.event import RecommendMediaSource as _SchemaRecommendMediaSource
from app.schemas.exception import TMDbException
from app.schemas.event import RecommendSourceEventData from app.schemas.event import RecommendSourceEventData
from app.schemas.exception import TMDbException
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
from app.schemas.types import ChainEventType from app.schemas.types import ChainEventType
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+10 -10
View File
@@ -4,25 +4,25 @@ import time
from typing import Any, AsyncIterator, Iterator, List, Optional from typing import Any, AsyncIterator, Iterator, List, Optional
from uuid import uuid4 from uuid import uuid4
from fastapi import Depends, Body, Request from fastapi import Body, Depends, Request
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from app.adapters.web.security.access import verify_resource_token, verify_token
from app.api.response import ResponseAPIRouter
from app.application.security.url import SecurityUtils
from app.chain.search import SearchChain
from app.domain.media import normalize_music_type
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.schemas.media import resolve_media_identity
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.search import SearchLastContextData as _SchemaSearchLastContextData from app.schemas.search import SearchLastContextData as _SchemaSearchLastContextData
from app.schemas.search import SearchRecommendStatusData as _SchemaSearchRecommendStatusData from app.schemas.search import SearchRecommendStatusData as _SchemaSearchRecommendStatusData
from app.schemas.search import SubtitleInfo as _SchemaSubtitleInfo from app.schemas.search import SubtitleInfo as _SchemaSubtitleInfo
from app.schemas.system import TorrentInfo as _SchemaTorrentInfo from app.schemas.system import TorrentInfo as _SchemaTorrentInfo
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import Context as _SchemaContext
from app.api.response import ResponseAPIRouter
from app.chain.search import SearchChain
from app.adapters.web.security.access import verify_resource_token, verify_token
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
from app.domain.media import normalize_music_type from app.schemas.workflow import Context as _SchemaContext
from app.schemas.media import resolve_media_identity
from app.application.security.url import SecurityUtils
router = ResponseAPIRouter() router = ResponseAPIRouter()
+27 -28
View File
@@ -1,31 +1,9 @@
from typing import List, Any, Dict, Optional from typing import Annotated, Any, Dict, List, Optional
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from typing import Annotated
from app.schemas.common import JsonObject as _SchemaJsonObject
from app.schemas.response import Response as _SchemaResponse
from app.schemas.site import SiteAuth as _SchemaSiteAuth
from app.schemas.site import SiteCategory as _SchemaSiteCategory
from app.schemas.site import SiteCookieUpdate as _SchemaSiteCookieUpdate
from app.schemas.site import SiteIconData as _SchemaSiteIconData
from app.schemas.site import SiteMappingData as _SchemaSiteMappingData
from app.schemas.site import SiteStatistic as _SchemaSiteStatistic
from app.schemas.site import SiteUserData as _SchemaSiteUserData
from app.schemas.system import TorrentInfo as _SchemaTorrentInfo
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import Site as _SchemaSite
from app.api.response import ResponseAPIRouter
from app.application.site.mutation import SiteMutationCommand
from app.application.site.query import SiteQueryService
from app.api.endpoints.plugin import register_plugin_api
from app.chain.site import SiteChain
from app.chain.torrents import TorrentsChain
from app.application.commands import init_commands
from app.application.plugin.runtime import get_plugin_manager
from app.adapters.web.security.access import verify_token from app.adapters.web.security.access import verify_token
from app.api.principal import ApiPrincipal from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.application.configuration import get_configured_system_config
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_manage_user_async, get_current_active_manage_user_async,
@@ -37,13 +15,34 @@ from app.api.dependencies.site import (
get_site_query_service, get_site_query_service,
get_site_sync_query_service, get_site_sync_query_service,
) )
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.api.endpoints.plugin import register_plugin_api
from app.runtime.log import logger from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.commands import init_commands
from app.application.configuration import get_configured_system_config
from app.application.plugin.runtime import get_plugin_manager
from app.application.scheduling import get_scheduler from app.application.scheduling import get_scheduler
from app.schemas.types import SystemConfigKey, MediaType from app.application.site.mutation import SiteMutationCommand
from app.application.site.query import SiteQueryService
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain.site import SiteChain
from app.chain.torrents import TorrentsChain
from app.domain import site as site_rules from app.domain import site as site_rules
from app.api.context import get_background_task_registry, resolve_background_task_registry from app.runtime.log import logger
from app.runtime.tasks import TaskRegistry from app.runtime.tasks import TaskRegistry
from app.schemas.common import JsonObject as _SchemaJsonObject
from app.schemas.response import Response as _SchemaResponse
from app.schemas.site import SiteAuth as _SchemaSiteAuth
from app.schemas.site import SiteCategory as _SchemaSiteCategory
from app.schemas.site import SiteCookieUpdate as _SchemaSiteCookieUpdate
from app.schemas.site import SiteIconData as _SchemaSiteIconData
from app.schemas.site import SiteMappingData as _SchemaSiteMappingData
from app.schemas.site import SiteStatistic as _SchemaSiteStatistic
from app.schemas.site import SiteUserData as _SchemaSiteUserData
from app.schemas.system import TorrentInfo as _SchemaTorrentInfo
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.types import MediaType, SystemConfigKey
from app.schemas.workflow import Site as _SchemaSite
router = ResponseAPIRouter() router = ResponseAPIRouter()
+11 -11
View File
@@ -7,22 +7,22 @@ from typing import Any, Dict, List, Optional
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from starlette.responses import FileResponse, Response from starlette.responses import FileResponse, Response
from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.application.configuration import get_api_runtime_config_snapshot
from app.api.principal import ApiPrincipal
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_superuser, get_current_active_superuser,
) )
from app.runtime.progress import ProgressHelper from app.api.principal import ApiPrincipal
from app.schemas.types import ProgressKey from app.api.response import ResponseAPIRouter
from app.application.configuration import get_api_runtime_config_snapshot
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.foundation import text as text_tools from app.foundation import text as text_tools
from app.runtime.progress import ProgressHelper
from app.schemas.common import ManageRequest as _SchemaManageRequest
from app.schemas.response import Response as _SchemaResponse
from app.schemas.types import ProgressKey
from app.schemas.workflow import FileItem as _SchemaFileItem
router = ResponseAPIRouter() router = ResponseAPIRouter()
+41 -41
View File
@@ -1,47 +1,14 @@
from typing import List, Any, Annotated, Optional from typing import Annotated, Any, List, Optional
import cn2an import cn2an
from fastapi import Request, Depends, HTTPException, Header from fastapi import Depends, Header, HTTPException, Request
from app.schemas.common import IdData as _SchemaIdData from app.adapters.external.server import MoviePilotServerHelper
from app.schemas.response import Response as _SchemaResponse from app.adapters.web.security.access import verify_apitoken, verify_token
from app.schemas.subscribe import SubscrbieInfo as _SchemaSubscrbieInfo
from app.schemas.subscribe import SubscribeShare as _SchemaSubscribeShare
from app.schemas.subscribe import SubscribeShareStatistics as _SchemaSubscribeShareStatistics
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.schemas.workflow import Subscribe as _SchemaSubscribe
from app.api.response import ResponseAPIRouter
from app.api.context import ( from app.api.context import (
get_background_task_registry, get_background_task_registry,
resolve_background_task_registry, resolve_background_task_registry,
) )
from app.chain.subscribe import SubscribeChain
from app.runtime.events import eventmanager
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.application.subscription.delete import (
DeleteSubscribeCommand,
SubscribeDeletionActor,
)
from app.application.subscription.identity import (
DeleteSubscriptionsByIdentityCommand,
)
from app.application.subscription.search import (
SearchSubscriptionsCommand,
SubscribeSearchActor,
)
from app.api.principal import ApiPrincipal
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.mutation import (
SubscriptionActor,
SubscriptionMutationService,
)
from app.application.configuration import (
get_api_runtime_config_snapshot,
get_configured_system_config,
)
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_user, get_current_active_user,
get_current_active_user_async, get_current_active_user_async,
@@ -50,23 +17,56 @@ from app.api.dependencies.subscription import (
get_delete_subscribe_command, get_delete_subscribe_command,
get_delete_subscriptions_by_identity_command, get_delete_subscriptions_by_identity_command,
get_search_subscriptions_command, get_search_subscriptions_command,
get_subscription_query_service,
get_subscription_mutation_service, get_subscription_mutation_service,
get_subscription_query_service,
get_subscription_sync_mutation_service, get_subscription_sync_mutation_service,
) )
from app.adapters.external.server import MoviePilotServerHelper from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.configuration import (
get_api_runtime_config_snapshot,
get_configured_system_config,
)
from app.application.scheduling import get_scheduler from app.application.scheduling import get_scheduler
from app.application.subscription.delete import (
DeleteSubscribeCommand,
SubscribeDeletionActor,
)
from app.application.subscription.identity import (
DeleteSubscriptionsByIdentityCommand,
)
from app.application.subscription.mutation import (
SubscriptionActor,
SubscriptionMutationService,
)
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.search import (
SearchSubscriptionsCommand,
SubscribeSearchActor,
)
from app.chain.subscribe import SubscribeChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.runtime.events import eventmanager
from app.runtime.tasks import TaskRegistry from app.runtime.tasks import TaskRegistry
from app.schemas.common import IdData as _SchemaIdData
from app.schemas.event import SubscribeModifiedEventData from app.schemas.event import SubscribeModifiedEventData
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.response import Response as _SchemaResponse
from app.schemas.subscribe import SubscrbieInfo as _SchemaSubscrbieInfo
from app.schemas.subscribe import SubscribeShare as _SchemaSubscribeShare
from app.schemas.subscribe import SubscribeShareStatistics as _SchemaSubscribeShareStatistics
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.types import ( from app.schemas.types import (
MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING, MUSIC_ENTITY_RECORDING,
EventType,
MediaSource, MediaSource,
MediaType, MediaType,
EventType,
SystemConfigKey, SystemConfigKey,
) )
from app.schemas.media import normalize_media_source, resolve_media_identity from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.schemas.workflow import Subscribe as _SchemaSubscribe
router = ResponseAPIRouter() router = ResponseAPIRouter()
+61 -61
View File
@@ -6,25 +6,76 @@ import zipfile
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Union, Annotated from typing import Annotated, Any, Optional, Union
from urllib.parse import urljoin, urlparse from urllib.parse import urlparse
import aiofiles import aiofiles
import anyio import anyio
import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用 import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用
from anyio import Path as AsyncPath from anyio import Path as AsyncPath
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from fastapi import Body, Depends, Header, HTTPException, Request, Response
from fastapi import Body, Depends, HTTPException, Header, Request, Response
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from app.adapters.external.market import (
PLUGIN_MARKET_WIKI_URL,
extract_plugin_market_repos_from_wiki,
merge_plugin_market_repos,
split_plugin_market_repo_urls,
)
from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.network.http import AsyncRequestUtils, RequestUtils
from app.adapters.system import rust as rust_accel
from app.adapters.system.update import system_update_manager
from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
get_current_active_user_async,
)
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.backup import DatabaseBackupInProgressError
from app.application.configuration import (
get_configured_system_config,
get_runtime_settings,
)
from app.application.database import get_database_governance
from app.application.image import ImageHelper
from app.application.messaging.message import MessageHelper
from app.application.module import get_module_manager
from app.application.network import NetworkTestService
from app.application.plugin.runtime import plugin_system_config_mutation
from app.application.rules import RuleHelper
from app.application.scheduling import get_scheduler
from app.application.security.url import SecurityUtils
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain.media import MediaChain
from app.chain.mediaserver import MediaServerChain
from app.chain.search import SearchChain
from app.domain.metainfo import MetaInfo
from app.foundation.crypto import HashUtils
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
from app.foundation.url import UrlUtils
from app.runtime.events import eventmanager
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.localization import LocaleHelper
from app.runtime.log import logger
from app.runtime.progress import AsyncProgressHelper
from app.runtime.scheduling import TimerUtils
from app.runtime.state import SystemHelper
from app.runtime.stop import runtime_stop_state
from app.runtime.config import global_vars
from app.runtime.version import get_app_version, get_frontend_version
from app.schemas.common import JsonObject as _SchemaJsonObject from app.schemas.common import JsonObject as _SchemaJsonObject
from app.schemas.common import JsonObjectList as _SchemaJsonObjectList from app.schemas.common import JsonObjectList as _SchemaJsonObjectList
from app.schemas.common import TimeData as _SchemaTimeData from app.schemas.common import TimeData as _SchemaTimeData
from app.schemas.common import ValueData as _SchemaValueData from app.schemas.common import ValueData as _SchemaValueData
from app.schemas.event import ConfigChangeEventData
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.system import NetTestTarget as _SchemaNetTestTarget
from app.schemas.system import DatabaseBackupArtifactData as _SchemaDatabaseBackupArtifactData from app.schemas.system import DatabaseBackupArtifactData as _SchemaDatabaseBackupArtifactData
from app.schemas.system import DatabaseBackupVerificationData as _SchemaDatabaseBackupVerificationData from app.schemas.system import DatabaseBackupVerificationData as _SchemaDatabaseBackupVerificationData
from app.schemas.system import NetTestTarget as _SchemaNetTestTarget
from app.schemas.system import PluginMarketSyncData as _SchemaPluginMarketSyncData from app.schemas.system import PluginMarketSyncData as _SchemaPluginMarketSyncData
from app.schemas.system import PluginMarketSyncRequest as _SchemaPluginMarketSyncRequest from app.schemas.system import PluginMarketSyncRequest as _SchemaPluginMarketSyncRequest
from app.schemas.system import RuleTestData as _SchemaRuleTestData from app.schemas.system import RuleTestData as _SchemaRuleTestData
@@ -33,58 +84,7 @@ from app.schemas.system import SystemModuleListData as _SchemaSystemModuleListDa
from app.schemas.system import SystemUpdateStatus as _SchemaSystemUpdateStatus from app.schemas.system import SystemUpdateStatus as _SchemaSystemUpdateStatus
from app.schemas.system import TorrentInfo as _SchemaTorrentInfo from app.schemas.system import TorrentInfo as _SchemaTorrentInfo
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.api.response import ResponseAPIRouter from app.schemas.types import EventType, SystemConfigKey
from app.chain.media import MediaChain
from app.chain.mediaserver import MediaServerChain
from app.chain.search import SearchChain
from app.chain.system import SystemChain
from app.runtime.config import global_vars
from app.runtime.events import eventmanager
from app.domain.metainfo import MetaInfo
from app.application.module import get_module_manager
from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
from app.api.principal import ApiPrincipal
from app.application.configuration import (
get_configured_system_config,
get_runtime_settings,
)
from app.application.backup import DatabaseBackupInProgressError
from app.application.database import get_database_governance
from app.application.plugin.runtime import plugin_system_config_mutation
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
get_current_active_user_async,
)
from app.application.image import ImageHelper
from app.runtime.localization import LocaleHelper
from app.adapters.external.market import (
PLUGIN_MARKET_WIKI_URL,
extract_plugin_market_repos_from_wiki,
merge_plugin_market_repos,
split_plugin_market_repo_urls,
)
from app.application.messaging.message import MessageHelper
from app.runtime.progress import AsyncProgressHelper
from app.runtime.scheduling import TimerUtils
from app.application.rules import RuleHelper
from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.state import SystemHelper
from app.runtime.log import logger
from app.runtime.execution import run_in_threadpool_to_completion
from app.application.scheduling import get_scheduler
from app.schemas.event import ConfigChangeEventData
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.types import SystemConfigKey, EventType
from app.foundation.crypto import HashUtils
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
from app.adapters.system import rust as rust_accel
from app.adapters.system.update import system_update_manager
from app.application.security.url import SecurityUtils
from app.application.network import NetworkTestService
from app.foundation.url import UrlUtils
from app.runtime.version import get_app_version, get_frontend_version
router = ResponseAPIRouter() router = ResponseAPIRouter()
@@ -1042,7 +1042,7 @@ async def get_progress(
async def event_generator(): async def event_generator():
try: try:
while not global_vars.is_system_stopped: while not runtime_stop_state.is_system_stopped:
if await request.is_disconnected(): if await request.is_disconnected():
break break
detail = await progress.get(locale=locale) detail = await progress.get(locale=locale)
@@ -1233,7 +1233,7 @@ async def get_message(
async def event_generator(): async def event_generator():
try: try:
while not global_vars.is_system_stopped: while not runtime_stop_state.is_system_stopped:
if await request.is_disconnected(): if await request.is_disconnected():
break break
detail = message.get(role) detail = message.get(role)
@@ -1314,7 +1314,7 @@ async def _get_logging_impl(
initial_stat = await log_path.stat() initial_stat = await log_path.stat()
initial_size = initial_stat.st_size initial_size = initial_stat.st_size
# 实时监听新日志,使用更短的轮询间隔 # 实时监听新日志,使用更短的轮询间隔
while not global_vars.is_system_stopped: while not runtime_stop_state.is_system_stopped:
if await request.is_disconnected(): if await request.is_disconnected():
break break
# 检查文件是否有新内容 # 检查文件是否有新内容
@@ -1413,7 +1413,7 @@ async def latest_version(_: _SchemaTokenPayload = Depends(verify_token)):
version_res = await AsyncRequestUtils( version_res = await AsyncRequestUtils(
proxies=get_runtime_settings().get("PROXY"), proxies=get_runtime_settings().get("PROXY"),
headers=get_runtime_settings().get("GITHUB_HEADERS"), headers=get_runtime_settings().get("GITHUB_HEADERS"),
).get_res(f"https://api.github.com/repos/jxxghp/MoviePilot/releases") ).get_res("https://api.github.com/repos/jxxghp/MoviePilot/releases")
if version_res is not None and version_res.status_code == 200: if version_res is not None and version_res.status_code == 200:
ver_json = version_res.json() ver_json = version_res.json()
if ver_json: if ver_json:
+8 -9
View File
@@ -1,21 +1,20 @@
from typing import List, Any, Optional from typing import Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import get_current_active_superuser_async
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_api_runtime_config_snapshot, get_configured_system_config
from app.chain.tmdb import TmdbChain
from app.schemas.context import MediaPerson as _SchemaMediaPerson from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.schemas.tmdb import TmdbRecognitionCacheData as _SchemaTmdbRecognitionCacheData from app.schemas.tmdb import TmdbRecognitionCacheData as _SchemaTmdbRecognitionCacheData
from app.schemas.tmdb import TmdbSeason as _SchemaTmdbSeason from app.schemas.tmdb import TmdbSeason as _SchemaTmdbSeason
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.tmdb import TmdbChain
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import verify_token
from app.application.configuration import get_configured_system_config
from app.api.dependencies.auth import get_current_active_superuser_async
from app.schemas.types import MediaType, SystemConfigKey from app.schemas.types import MediaType, SystemConfigKey
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
router = ResponseAPIRouter() router = ResponseAPIRouter()
+10 -10
View File
@@ -2,24 +2,24 @@ from typing import Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.cache import TorrentCacheData as _SchemaTorrentCacheData
from app.schemas.cache import TorrentReidentifyData as _SchemaTorrentReidentifyData
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.torrents import TorrentsChain
from app.application.configuration import get_api_runtime_config_snapshot
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_superuser, get_current_active_superuser,
get_current_active_superuser_async, get_current_active_superuser_async,
) )
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.torrent_cache import TorrentCacheRecognitionService
from app.chain.media import MediaChain
from app.chain.torrents import TorrentsChain
from app.foundation.crypto import HashUtils
from app.schemas.cache import TorrentCacheData as _SchemaTorrentCacheData
from app.schemas.cache import TorrentReidentifyData as _SchemaTorrentReidentifyData
from app.schemas.media import resolve_media_identity
from app.schemas.response import Response as _SchemaResponse
from app.schemas.types import ( from app.schemas.types import (
MediaSource, MediaSource,
MusicTargetEntityType, MusicTargetEntityType,
) )
from app.foundation.crypto import HashUtils
from app.schemas.media import resolve_media_identity
from app.application.torrent_cache import TorrentCacheRecognitionService
router = ResponseAPIRouter() router = ResponseAPIRouter()
+17 -18
View File
@@ -1,34 +1,33 @@
from pathlib import Path from pathlib import Path
from typing import Any, List, Annotated, Optional from typing import Annotated, Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.adapters.web.security.access import verify_apitoken, verify_token
from app.api.dependencies.auth import get_current_active_manage_user
from app.api.dependencies.history import get_transfer_history_lookup_service
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.directory import DirectoryHelper
from app.application.history import TransferHistoryLookupService
from app.chain.media import MediaChain
from app.chain.transfer import TransferChain
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.common import NameData as _SchemaNameData from app.schemas.common import NameData as _SchemaNameData
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.schemas.transfer import EpisodeFormat as _SchemaEpisodeFormat from app.schemas.transfer import EpisodeFormat as _SchemaEpisodeFormat
from app.schemas.transfer import EpisodeFormatRecommendData as _SchemaEpisodeFormatRecommendData from app.schemas.transfer import EpisodeFormatRecommendData as _SchemaEpisodeFormatRecommendData
from app.schemas.transfer import EpisodeFormatRecommendItem, ManualTransferItem
from app.schemas.transfer import ManualTransferHistoryInfo as _SchemaManualTransferHistoryInfo from app.schemas.transfer import ManualTransferHistoryInfo as _SchemaManualTransferHistoryInfo
from app.schemas.transfer import ManualTransferResultData as _SchemaManualTransferResultData from app.schemas.transfer import ManualTransferResultData as _SchemaManualTransferResultData
from app.schemas.transfer import ManualTransferTargetPath as _SchemaManualTransferTargetPath from app.schemas.transfer import ManualTransferTargetPath as _SchemaManualTransferTargetPath
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
from app.schemas.transfer import TransferJob as _SchemaTransferJob from app.schemas.transfer import TransferJob as _SchemaTransferJob
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.transfer import TransferChain
from app.runtime.config import global_vars
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import verify_token, verify_apitoken
from app.api.dependencies.auth import get_current_active_manage_user
from app.api.dependencies.history import get_transfer_history_lookup_service
from app.application.directory import DirectoryHelper
from app.application.history import TransferHistoryLookupService
from app.runtime.log import logger
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.schemas.workflow import FileItem from app.schemas.workflow import FileItem
from app.schemas.transfer import ManualTransferItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.schemas.transfer import EpisodeFormatRecommendItem
router = ResponseAPIRouter() router = ResponseAPIRouter()
@@ -102,7 +101,7 @@ async def remove_queue(
""" """
TransferChain().remove_from_queue(fileitem) TransferChain().remove_from_queue(fileitem)
# 取消整理 # 取消整理
global_vars.stop_transfer(fileitem.path) runtime_stop_state.stop_transfer(fileitem.path)
return _SchemaResponse(success=True) return _SchemaResponse(success=True)
@@ -398,7 +397,7 @@ def _execute_manual_transfer(
elif transer_item.fileitem: elif transer_item.fileitem:
src_fileitems = [transer_item.fileitem] src_fileitems = [transer_item.fileitem]
else: else:
return _SchemaResponse(success=False, message=f"缺少参数") return _SchemaResponse(success=False, message="缺少参数")
dedup_fileitems: List[FileItem] = [] dedup_fileitems: List[FileItem] = []
seen_paths = set() seen_paths = set()
+10 -10
View File
@@ -2,23 +2,23 @@ import base64
import re import re
from typing import Annotated, Any, List, Union from typing import Annotated, Any, List, Union
from fastapi import Body, Depends, HTTPException, UploadFile, File from fastapi import Body, Depends, File, HTTPException, UploadFile
from app.api.dependencies.auth import (
get_current_active_superuser_async,
get_current_active_user_async,
get_user_service,
)
from app.api.response import ResponseAPIRouter
from app.application.security.token import PasswordTooLongError, get_password_hash
from app.application.security.user import UserService
from app.application.security.userconfig import get_configured_user_configuration
from app.schemas.common import FileNameData as _SchemaFileNameData from app.schemas.common import FileNameData as _SchemaFileNameData
from app.schemas.common import ValueData as _SchemaValueData from app.schemas.common import ValueData as _SchemaValueData
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.user import User as _SchemaUser from app.schemas.user import User as _SchemaUser
from app.schemas.user import UserCreate as _SchemaUserCreate from app.schemas.user import UserCreate as _SchemaUserCreate
from app.schemas.user import UserUpdate as _SchemaUserUpdate from app.schemas.user import UserUpdate as _SchemaUserUpdate
from app.api.response import ResponseAPIRouter
from app.application.security.token import PasswordTooLongError, get_password_hash
from app.application.security.user import UserService
from app.api.dependencies.auth import (
get_current_active_superuser_async,
get_current_active_user_async,
get_user_service,
)
from app.application.security.userconfig import get_configured_user_configuration
router = ResponseAPIRouter() router = ResponseAPIRouter()
+4 -4
View File
@@ -1,13 +1,13 @@
from typing import Any, Annotated from typing import Annotated, Any
from fastapi import Depends, Request from fastapi import Depends, Request
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.webhook import WebhookChain
from app.adapters.web.security.access import verify_apitoken from app.adapters.web.security.access import verify_apitoken
from app.api.context import get_background_task_registry, resolve_background_task_registry from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.api.response import ResponseAPIRouter
from app.chain.webhook import WebhookChain
from app.runtime.tasks import TaskRegistry from app.runtime.tasks import TaskRegistry
from app.schemas.response import Response as _SchemaResponse
router = ResponseAPIRouter() router = ResponseAPIRouter()
+18 -18
View File
@@ -1,22 +1,8 @@
from typing import List, Any, Optional from typing import Any, List, Optional
from fastapi import Depends from fastapi import Depends
from app.schemas.response import Response as _SchemaResponse from app.adapters.external.server import MoviePilotServerHelper
from app.schemas.workflow import NameValueOption as _SchemaNameValueOption
from app.schemas.workflow import PluginWorkflowActionGroup as _SchemaPluginWorkflowActionGroup
from app.schemas.workflow import Workflow as _SchemaWorkflow
from app.schemas.workflow import WorkflowActionDefinition as _SchemaWorkflowActionDefinition
from app.schemas.workflow import WorkflowShare as _SchemaWorkflowShare
from app.api.response import ResponseAPIRouter
from app.application.workflow import (
WorkflowDefinitionCommand,
WorkflowMutationCommand,
WorkflowQueryService,
get_workflow_manager,
)
from app.chain.workflow import WorkflowChain
from app.application.plugin.runtime import get_plugin_manager
from app.api.dependencies.auth import ( from app.api.dependencies.auth import (
get_current_active_manage_user, get_current_active_manage_user,
get_current_active_manage_user_async, get_current_active_manage_user_async,
@@ -26,8 +12,22 @@ from app.api.dependencies.workflow import (
get_workflow_mutation_command, get_workflow_mutation_command,
get_workflow_query_service, get_workflow_query_service,
) )
from app.adapters.external.server import MoviePilotServerHelper from app.api.response import ResponseAPIRouter
from app.schemas.types import EventType, EVENT_TYPE_NAMES from app.application.plugin.runtime import get_plugin_manager
from app.application.workflow import (
WorkflowDefinitionCommand,
WorkflowMutationCommand,
WorkflowQueryService,
get_workflow_manager,
)
from app.chain.workflow import WorkflowChain
from app.schemas.response import Response as _SchemaResponse
from app.schemas.types import EVENT_TYPE_NAMES, EventType
from app.schemas.workflow import NameValueOption as _SchemaNameValueOption
from app.schemas.workflow import PluginWorkflowActionGroup as _SchemaPluginWorkflowActionGroup
from app.schemas.workflow import Workflow as _SchemaWorkflow
from app.schemas.workflow import WorkflowActionDefinition as _SchemaWorkflowActionDefinition
from app.schemas.workflow import WorkflowShare as _SchemaWorkflowShare
router = ResponseAPIRouter() router = ResponseAPIRouter()
-1
View File
@@ -6,7 +6,6 @@ from typing import Any
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
SSE_HEADERS = { SSE_HEADERS = {
"Cache-Control": "no-cache, no-transform", "Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive", "Connection": "keep-alive",
-1
View File
@@ -11,7 +11,6 @@ from starlette.responses import Response as StarletteResponse
from app.schemas.common import JsonData from app.schemas.common import JsonData
from app.schemas.response import Response, ValidationIssue from app.schemas.response import Response, ValidationIssue
ERROR_RESPONSES: dict[int, dict[str, Any]] = { ERROR_RESPONSES: dict[int, dict[str, Any]] = {
400: {"model": Response[None], "description": "请求错误"}, 400: {"model": Response[None], "description": "请求错误"},
401: {"model": Response[None], "description": "未认证"}, 401: {"model": Response[None], "description": "未认证"},
+13 -14
View File
@@ -1,8 +1,19 @@
from typing import List, Optional, Annotated from typing import Annotated, List, Optional
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, Depends, HTTPException
from app.adapters.web.security.access import verify_apikey
from app.api.dependencies.subscription import get_servarr_subscription_service
from app.api.response import ERROR_RESPONSES
from app.application.servarr import ServarrSubscription, ServarrSubscriptionService
from app.chain.media import MediaChain
from app.chain.subscribe import SubscribeChain
from app.chain.tvdb import TvdbChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.runtime.version import get_app_version
from app.schemas.response import Response as _SchemaResponse from app.schemas.response import Response as _SchemaResponse
from app.schemas.servarr import RadarrMovie, SonarrSeries
from app.schemas.servarr import RadarrMovie as _SchemaRadarrMovie from app.schemas.servarr import RadarrMovie as _SchemaRadarrMovie
from app.schemas.servarr import ServarrIdResponse as _SchemaServarrIdResponse from app.schemas.servarr import ServarrIdResponse as _SchemaServarrIdResponse
from app.schemas.servarr import ServarrLanguageProfile as _SchemaServarrLanguageProfile from app.schemas.servarr import ServarrLanguageProfile as _SchemaServarrLanguageProfile
@@ -11,19 +22,7 @@ from app.schemas.servarr import ServarrRootFolder as _SchemaServarrRootFolder
from app.schemas.servarr import ServarrSystemStatus as _SchemaServarrSystemStatus from app.schemas.servarr import ServarrSystemStatus as _SchemaServarrSystemStatus
from app.schemas.servarr import ServarrTag as _SchemaServarrTag from app.schemas.servarr import ServarrTag as _SchemaServarrTag
from app.schemas.servarr import SonarrSeries as _SchemaSonarrSeries from app.schemas.servarr import SonarrSeries as _SchemaSonarrSeries
from app.api.response import ERROR_RESPONSES
from app.chain.media import MediaChain
from app.chain.subscribe import SubscribeChain
from app.chain.tvdb import TvdbChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.application.servarr import ServarrSubscription, ServarrSubscriptionService
from app.adapters.web.security.access import verify_apikey
from app.api.dependencies.subscription import get_servarr_subscription_service
from app.schemas.servarr import RadarrMovie
from app.schemas.servarr import SonarrSeries
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
from app.runtime.version import get_app_version
arr_router = APIRouter(tags=["servarr"], responses=ERROR_RESPONSES) arr_router = APIRouter(tags=["servarr"], responses=ERROR_RESPONSES)
+4 -4
View File
@@ -9,15 +9,15 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Reque
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from app.api.response import ERROR_RESPONSES
from app.application.configuration import get_api_runtime_config_snapshot
from app.foundation.crypto import CryptoJsUtils, HashUtils
from app.runtime.log import logger
from app.schemas.servcookie import CookieActionResponse as _SchemaCookieActionResponse from app.schemas.servcookie import CookieActionResponse as _SchemaCookieActionResponse
from app.schemas.servcookie import CookieData as _SchemaCookieData from app.schemas.servcookie import CookieData as _SchemaCookieData
from app.schemas.servcookie import CookieDecryptedPayload as _SchemaCookieDecryptedPayload from app.schemas.servcookie import CookieDecryptedPayload as _SchemaCookieDecryptedPayload
from app.schemas.servcookie import CookieEncryptedPayload as _SchemaCookieEncryptedPayload from app.schemas.servcookie import CookieEncryptedPayload as _SchemaCookieEncryptedPayload
from app.schemas.servcookie import CookiePassword as _SchemaCookiePassword from app.schemas.servcookie import CookiePassword as _SchemaCookiePassword
from app.api.response import ERROR_RESPONSES
from app.application.configuration import get_api_runtime_config_snapshot
from app.runtime.log import logger
from app.foundation.crypto import CryptoJsUtils, HashUtils
class GzipRequest(Request): class GzipRequest(Request):
+2
View File
@@ -9,6 +9,7 @@ from typing import Any, Optional
from app.application.chain.data import ChainDataPorts from app.application.chain.data import ChainDataPorts
from app.application.chain.durable_events import ChainDurableEventWriter from app.application.chain.durable_events import ChainDurableEventWriter
from app.application.configuration import ChainRuntimeConfig from app.application.configuration import ChainRuntimeConfig
from app.runtime.stop import StopState, runtime_stop_state
MessageQueueFactory = Callable[[Callable[..., Any]], Any] MessageQueueFactory = Callable[[Callable[..., Any]], Any]
@@ -34,6 +35,7 @@ class ChainRuntimeContext:
configuration: ChainRuntimeConfig = field( configuration: ChainRuntimeConfig = field(
default_factory=lambda: ChainRuntimeConfig(media_extensions=()) default_factory=lambda: ChainRuntimeConfig(media_extensions=())
) )
stop_state: StopState = field(default_factory=lambda: runtime_stop_state)
def _unconfigured_chain_runtime_context() -> ChainRuntimeContext: def _unconfigured_chain_runtime_context() -> ChainRuntimeContext:
+8 -10
View File
@@ -10,26 +10,24 @@ import time
from contextvars import Context, copy_context from contextvars import Context, copy_context
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
from typing import Any, Literal, Optional, List, Dict, Protocol, Union from typing import Any, Callable, Dict, List, Literal, Optional, Protocol, Union
from typing import Callable
from jinja2 import Template from jinja2 import Template
from app.runtime.cache import TTLCache from app.application.configuration import get_configured_system_config
from app.runtime.config import global_vars
from app.domain.context import MediaInfo, MusicInfo, TorrentInfo from app.domain.context import MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.application.configuration import get_configured_system_config from app.foundation import size as size_tools
from app.foundation.crypto import HashUtils
from app.foundation.singleton import Singleton, SingletonClass
from app.runtime.cache import TTLCache
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.message import Message from app.schemas.message import Message
from app.schemas.tmdb import TmdbEpisode from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import TransferInfo from app.schemas.transfer import TransferInfo
from app.schemas.types import MUSIC_ENTITY_ALBUM, SystemConfigKey from app.schemas.types import MUSIC_ENTITY_ALBUM, SystemConfigKey
from app.foundation.singleton import Singleton, SingletonClass
from app.foundation import size as size_tools
from app.foundation.crypto import HashUtils
# 专辑名尾部的括号年份标记;重命名模板会独立追加 `({{year}})` # 专辑名尾部的括号年份标记;重命名模板会独立追加 `({{year}})`
# 标签或目录名中自带的尾部年份若不剥离,会生成重复年份的目录名(issue #6355) # 标签或目录名中自带的尾部年份若不剥离,会生成重复年份的目录名(issue #6355)
@@ -998,7 +996,7 @@ class MessageQueueManager(metaclass=SingletonClass):
current_time = datetime.now() current_time = datetime.now()
if self._is_in_scheduled_time(current_time): if self._is_in_scheduled_time(current_time):
while self._running and not self.queue.empty(): while self._running and not self.queue.empty():
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if not self._is_in_scheduled_time(datetime.now()): if not self._is_in_scheduled_time(datetime.now()):
break break
+14 -15
View File
@@ -5,7 +5,7 @@ import traceback
from abc import ABCMeta from abc import ABCMeta
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path 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.context import ChainRuntimeContext, get_chain_runtime_context
from app.application.chain.data import get_chain_data_ports 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.context import Context, MediaInfo, SubtitleInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.runtime.log import logger 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.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 ( from app.schemas.types import (
TorrentStatus,
MediaType,
MediaSourceSelection,
MediaImageType,
EventType, EventType,
MediaImageType,
MediaSourceSelection,
MediaType,
TorrentStatus,
) )
from app.schemas.workflow import FileItem
class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin, class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
@@ -57,6 +55,7 @@ class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
self.filecache = context.file_cache self.filecache = context.file_cache
self.async_filecache = context.async_file_cache self.async_filecache = context.async_file_cache
self.runtime_config = context.configuration self.runtime_config = context.configuration
self.stop_state = context.stop_state
self.data_ports = context.data_ports or get_chain_data_ports() self.data_ports = context.data_ports or get_chain_data_ports()
self.durable_event_writer = context.durable_event_writer self.durable_event_writer = context.durable_event_writer
self._module_dispatcher = context.module_dispatcher_factory( 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 typing import Optional, Tuple, Union
from app.chain._contracts import InteractionMixinHost
from app.schemas.types import NotificationChannel from app.schemas.types import NotificationChannel
class InteractionChainMixin: class InteractionChainMixin:
__mixin_host_protocol__ = InteractionMixinHost
""" """
斜杠命令交互四件套委托remote_list / parse_callback / 斜杠命令交互四件套委托remote_list / parse_callback /
handle_callback_interaction / handle_text_interaction 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 typing import Any, Dict, List, Optional, Union
from app.application.chain.data import get_chain_user_port 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.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.foundation.identity import normalize_internal_user_id 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.runtime.log import logger
from app.schemas.message import MessageResponse from app.schemas.message import Message, MessageResponse
from app.schemas.message import Message
from app.schemas.transfer import TransferInfo
from app.schemas.notification import ChannelCapability, ChannelCapabilityManager from app.schemas.notification import ChannelCapability, ChannelCapabilityManager
from app.schemas.transfer import TransferInfo
from app.schemas.types import EventType, NotificationChannel from app.schemas.types import EventType, NotificationChannel
class MessageProcessingMixin: class MessageProcessingMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
"""消息输入/处理状态机与通知派发规范化。""" """消息输入/处理状态机与通知派发规范化。"""
def start_message_processing_status( def start_message_processing_status(
@@ -117,6 +118,7 @@ class MessageProcessingMixin:
class NotificationMixin: class NotificationMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
"""通知消息发送域:渲染、隔离路由、队列发送与消息编辑。""" """通知消息发送域:渲染、隔离路由、队列发送与消息编辑。"""
def post_message( def post_message(
+10 -3
View File
@@ -1,16 +1,22 @@
import copy import copy
from typing import Any, List, Optional, Tuple 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 ( from app.application.subscription.contract import (
build_subscribe_meta, build_subscribe_meta,
subscribe_media_key, subscribe_media_key,
) )
from app.application.torrent import TorrentHelper
from app.chain._contracts import MusicSubscribeMixinHost
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.chain.search import SearchChain 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.context import Context, MediaInfo, MusicInfo
from app.domain.media import MUSIC_SUBSCRIBABLE_TYPES from app.domain.media import MUSIC_SUBSCRIBABLE_TYPES
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
@@ -35,6 +41,7 @@ def _normalize_music_total_tracks(value: Any) -> Optional[int]:
class MusicSubscribeMixin: class MusicSubscribeMixin:
__mixin_host_protocol__ = MusicSubscribeMixinHost
""" """
音乐订阅功能域 mixin单曲/专辑目标识别实体快照同步候选筛选 音乐订阅功能域 mixin单曲/专辑目标识别实体快照同步候选筛选
择优下载与完成推进 择优下载与完成推进
+4 -2
View File
@@ -7,20 +7,22 @@
import copy import copy
from typing import Optional from typing import Optional
from app.runtime.execution import run_in_threadpool
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.application.configuration import get_configured_system_config 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.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic 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.events import Event
from app.runtime.execution import run_in_threadpool
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity from app.schemas.media import normalize_media_source, resolve_media_identity
from app.schemas.types import ChainEventType, MediaSource, MediaType, SystemConfigKey from app.schemas.types import ChainEventType, MediaSource, MediaType, SystemConfigKey
class RecognitionMixin: class RecognitionMixin:
__mixin_host_protocol__ = ChainRuntimeMixinHost
def _can_use_media_recognize_share( def _can_use_media_recognize_share(
self, self,
+22 -10
View File
@@ -7,21 +7,12 @@ TransferChain 中。mixin 方法运行时经 MRO 解析,共享 TransferChain
注意这里的方法均已去掉私有名前缀双下划线__ -> _因为 Python 的名字 注意这里的方法均已去掉私有名前缀双下划线__ -> _因为 Python 的名字
改编按定义类生效方法迁到 mixin __ 前缀会改变改编目标导致跨类调用失败 改编按定义类生效方法迁到 mixin __ 前缀会改变改编目标导致跨类调用失败
""" """
import asyncio
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union 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.adapters.system.host import SystemUtils
from app.application.agent import build_manual_redo_prompt, get_running_agent_manager 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 ( from app.application.chain.data import (
get_chain_download_history_port, get_chain_download_history_port,
get_chain_transfer_history_port, get_chain_transfer_history_port,
@@ -30,6 +21,18 @@ from app.application.configuration import (
get_chain_runtime_config_snapshot, get_chain_runtime_config_snapshot,
get_configured_system_config, 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.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase 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.config import global_vars
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.tasks import get_task_registry 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.message import Message
from app.schemas.tmdb import TmdbEpisode from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import EpisodeFormatRule as _SchemaEpisodeFormatRule
from app.schemas.transfer import TransferInfo from app.schemas.transfer import TransferInfo
from app.schemas.types import ( from app.schemas.types import (
MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ALBUM,
@@ -51,6 +55,7 @@ from app.schemas.types import (
ReplyMode, ReplyMode,
SystemConfigKey, SystemConfigKey,
) )
from app.schemas.workflow import FileItem
DownloadFiles = Any DownloadFiles = Any
DownloadHistory = Any DownloadHistory = Any
@@ -100,6 +105,7 @@ SUBTITLE_STEM_TAGS = {
class FileFilterMixin: class FileFilterMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod @staticmethod
def _requires_automatic_category(task: TransferTask) -> bool: def _requires_automatic_category(task: TransferTask) -> bool:
""" """
@@ -446,6 +452,7 @@ class FileFilterMixin:
class ScrapeBatchMixin: class ScrapeBatchMixin:
__mixin_host_protocol__ = TransferMixinHost
def _send_metadata_scrape_event( def _send_metadata_scrape_event(
self, task: TransferTask, transferinfo: TransferInfo self, task: TransferTask, transferinfo: TransferInfo
@@ -647,6 +654,7 @@ class ScrapeBatchMixin:
class EpisodeFormatMixin: class EpisodeFormatMixin:
__mixin_host_protocol__ = TransferMixinHost
def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Optional[str]: def recommend_name(self, meta: MetaBase, mediainfo: MediaInfo) -> Optional[str]:
""" """
@@ -837,6 +845,7 @@ class EpisodeFormatMixin:
class HistoryMatchMixin: class HistoryMatchMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod @staticmethod
def _match_download_file( def _match_download_file(
download_file: DownloadFiles, download_file: DownloadFiles,
@@ -1023,6 +1032,7 @@ class HistoryMatchMixin:
class FileKeyMixin: class FileKeyMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod @staticmethod
def _get_file_key(fileitem: FileItem) -> Tuple[str, str]: def _get_file_key(fileitem: FileItem) -> Tuple[str, str]:
""" """
@@ -1110,6 +1120,7 @@ class FileKeyMixin:
class ManualHistoryMixin: class ManualHistoryMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod @staticmethod
def _get_subscribe_custom_words( def _get_subscribe_custom_words(
history_record: Optional[DownloadHistory], history_record: Optional[DownloadHistory],
@@ -1234,6 +1245,7 @@ class ManualHistoryMixin:
class FailedRetryMixin: class FailedRetryMixin:
__mixin_host_protocol__ = TransferMixinHost
@staticmethod @staticmethod
def build_failed_transfer_buttons( def build_failed_transfer_buttons(
history_id: Optional[int], history_id: Optional[int],
+1 -1
View File
@@ -1,8 +1,8 @@
from typing import Optional from typing import Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.chain import ChainBase from app.chain import ChainBase
from app.domain.context import MediaInfo from app.domain.context import MediaInfo
from app.schemas.context import MediaPerson as _SchemaMediaPerson
class AniListChain(ChainBase): 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.chain import ChainBase
from app.domain.context import MediaInfo from app.domain.context import MediaInfo
from app.schemas.context import MediaPerson as _SchemaMediaPerson
class BangumiChain(ChainBase): 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 DownloaderInfo as _SchemaDownloaderInfo
from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.chain import ChainBase
class DashboardChain(ChainBase): class DashboardChain(ChainBase):
+1 -1
View File
@@ -1,9 +1,9 @@
from typing import Any, List, Optional from typing import Any, List, Optional
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.chain import ChainBase from app.chain import ChainBase
from app.domain.context import MediaInfo, MusicAlbumInfo, MusicInfo from app.domain.context import MediaInfo, MusicAlbumInfo, MusicInfo
from app.domain.meta.metamusic import MetaMusic 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 from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType
+44 -37
View File
@@ -6,18 +6,25 @@ import re
import shutil import shutil
import time import time
from pathlib import Path 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 urllib.parse import parse_qs, urlencode, urljoin, urlparse
from app.schemas.transfer import DownloaderTorrent as _SchemaDownloaderTorrent from app.adapters.network.http import RequestUtils
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf from app.adapters.system.host import SystemUtils
from app.schemas.workflow import FileItem as _SchemaFileItem 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 import ChainBase
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.chain.storage import StorageChain from app.chain.storage import StorageChain
from app.runtime.cache import FileCache from app.domain import episode as episode_rules
from app.runtime.config import global_vars
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain.context import ( from app.domain.context import (
Context, Context,
MediaInfo, MediaInfo,
@@ -25,36 +32,36 @@ from app.domain.context import (
SubtitleInfo, SubtitleInfo,
TorrentInfo, TorrentInfo,
) )
from app.runtime.events import eventmanager, Event
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo 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 size as size_tools
from app.foundation import text as text_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: if TYPE_CHECKING:
from typing import Any from typing import Any
@@ -1000,7 +1007,7 @@ class DownloadChain(ChainBase):
MediaType.MUSIC: set(), MediaType.MUSIC: set(),
} }
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
media_type = context.media_info.type media_type = context.media_info.type
if media_type not in downloaded_keys: if media_type not in downloaded_keys:
@@ -1657,7 +1664,7 @@ class DownloadChain(ChainBase):
for need_mid, need_season in need_seasons.items(): for need_mid, need_season in need_seasons.items():
# 循环种子 # 循环种子
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
# 媒体信息 # 媒体信息
media = context.media_info media = context.media_info
@@ -1801,7 +1808,7 @@ class DownloadChain(ChainBase):
need_episodes = list(range(start_episode, total_episode + 1)) need_episodes = list(range(start_episode, total_episode + 1))
# 循环种子 # 循环种子
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
# 媒体信息 # 媒体信息
media = context.media_info media = context.media_info
@@ -1889,7 +1896,7 @@ class DownloadChain(ChainBase):
continue continue
# 循环种子 # 循环种子
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
# 媒体信息 # 媒体信息
media = context.media_info media = context.media_info
@@ -2040,7 +2047,7 @@ class DownloadChain(ChainBase):
media_id=media_id, media_id=media_id,
episode_group=mediainfo.episode_group) episode_group=mediainfo.episode_group)
if not mediainfo: if not mediainfo:
logger.error(f"媒体信息识别失败!") logger.error("媒体信息识别失败!")
return False, {} return False, {}
if not mediainfo.seasons: if not mediainfo.seasons:
logger.error(f"媒体信息中没有季集信息:{mediainfo.title_year}") logger.error(f"媒体信息中没有季集信息:{mediainfo.title_year}")
+7 -7
View File
@@ -2,18 +2,18 @@ import math
import re import re
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from app.chain import ChainBase from app.application.chain.data import get_chain_user_port
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.directory import DirectoryHelper from app.application.directory import DirectoryHelper
from app.application.messaging.media import ( from app.application.messaging.media import (
PendingMediaInteraction, PendingMediaInteraction,
media_interaction_manager, media_interaction_manager,
) )
from app.application.torrent import TorrentHelper 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 episode as episode_rules
from app.domain import title as title_rules from app.domain import title as title_rules
from app.domain.context import Context, MediaInfo 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.runtime.log import logger
from app.schemas.download import DownloadDirectory from app.schemas.download import DownloadDirectory
from app.schemas.file import FileURI 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.mediaserver import NotExistMediaInfo
from app.schemas.message import Message 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.notification import ChannelCapabilityManager
from app.schemas.system import TransferDirectoryConf from app.schemas.system import TransferDirectoryConf
from app.schemas.types import MediaType, NotificationChannel from app.schemas.types import MediaType, NotificationChannel
+16 -16
View File
@@ -3,15 +3,15 @@ from pathlib import Path
from threading import Lock from threading import Lock
from typing import Any, Iterable, List, Optional, Tuple, Union from typing import Any, Iterable, List, Optional, Tuple, Union
from app.runtime.execution import run_in_threadpool from app.application.audio import AudioMetadataHelper
from app.schemas.event import MediaRecognizeConvertEventData as _SchemaMediaRecognizeConvertEventData 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 import ChainBase
from app.chain.acoustid import AcoustIdChain from app.chain.acoustid import AcoustIdChain
from app.chain.douban import DoubanChain from app.chain.douban import DoubanChain
from app.chain.musicbrainz import MusicBrainzChain, _MusicMetadataSourceChain from app.chain.musicbrainz import MusicBrainzChain, _MusicMetadataSourceChain
from app.chain.theaudiodb import TheAudioDbChain from app.chain.theaudiodb import TheAudioDbChain
from app.runtime.cache import async_fresh, fresh from app.domain import title as title_rules
from app.application.configuration import get_chain_runtime_config_snapshot
from app.domain.context import ( from app.domain.context import (
Context, Context,
MediaInfo, MediaInfo,
@@ -19,13 +19,18 @@ from app.domain.context import (
MusicArtistInfo, MusicArtistInfo,
MusicInfo, 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.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.application.audio import AudioMetadataHelper from app.foundation.singleton import Singleton
from app.application.music.catalog import MusicCatalogService 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.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 ( from app.schemas.types import (
MUSIC_ENTITY_RECORDING, MUSIC_ENTITY_RECORDING,
ChainEventType, ChainEventType,
@@ -33,11 +38,6 @@ from app.schemas.types import (
MediaSourceSelection, MediaSourceSelection,
MediaType, 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() recognize_lock = Lock()
@@ -786,9 +786,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
year = None year = None
# 结果赋值 # 结果赋值
if title == org_meta.name and year == org_meta.year: if title == org_meta.name and year == org_meta.year:
logger.info(f"辅助识别与原始识别结果一致,无需重新识别媒体信息") logger.info("辅助识别与原始识别结果一致,无需重新识别媒体信息")
return None return None
logger.info(f"辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...") logger.info("辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
org_meta.name = title org_meta.name = title
org_meta.year = year org_meta.year = year
org_meta.begin_season = season_number org_meta.begin_season = season_number
@@ -1725,9 +1725,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
year = None year = None
# 结果赋值 # 结果赋值
if title == org_meta.name and year == org_meta.year: if title == org_meta.name and year == org_meta.year:
logger.info(f"辅助识别与原始识别结果一致,无需重新识别媒体信息") logger.info("辅助识别与原始识别结果一致,无需重新识别媒体信息")
return None return None
logger.info(f"辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...") logger.info("辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...")
org_meta.name = title org_meta.name = title
org_meta.year = year org_meta.year = year
org_meta.begin_season = season_number org_meta.begin_season = season_number
+8 -11
View File
@@ -1,18 +1,15 @@
import threading import threading
from datetime import datetime 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.chain.data import get_chain_media_server_port
from app.application.mediaserver import get_mediaserver_configs 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.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() lock = threading.Lock()
@@ -355,7 +352,7 @@ class MediaServerChain(ChainBase):
library_media_total = library_media_counts.get(str(library.id)) library_media_total = library_media_counts.get(str(library.id))
library_count = 0 library_count = 0
for item in self.items(server=server_name, library_id=library.id): 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 return total_count, global_media_finished
if not item or not item.item_id: if not item or not item.item_id:
continue continue
@@ -554,7 +551,7 @@ class MediaServerChain(ChainBase):
global_media_total=global_media_total, global_media_total=global_media_total,
global_media_finished=global_media_finished, global_media_finished=global_media_finished,
) )
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
return return
total_count += server_count total_count += server_count
logger.info(f"媒体服务器 {server_name} 数据同步完成,总同步数量:{total_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 dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path 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 urllib.parse import unquote, urlparse
from app.adapters.network.http import RequestUtils
from app.application.agent import ( from app.application.agent import (
get_running_agent_manager, get_running_agent_manager,
is_audio_input_available, is_audio_input_available,
supports_image_input, supports_image_input,
transcribe_audio, 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.agent import agent_interaction_manager, parse_agent_choice_callback
from app.application.messaging.interaction import InteractionContext, InteractionDispatch from app.application.messaging.interaction import InteractionContext, InteractionDispatch
from app.application.messaging.media import media_interaction_manager 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.site import site_interaction_manager
from app.application.messaging.skill import SkillInteractionHandler, skill_interaction_manager from app.application.messaging.skill import SkillInteractionHandler, skill_interaction_manager
from app.application.messaging.subscribe import subscribe_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.runtime.log import logger
from app.schemas.message import IncomingMessage from app.runtime.tasks import get_task_registry
from app.schemas.message import Message from app.schemas.message import IncomingMessage, Message
from app.schemas.notification import ChannelCapabilityManager from app.schemas.notification import ChannelCapabilityManager
from app.schemas.types import EventType, NotificationChannel from app.schemas.types import EventType, NotificationChannel
from app.adapters.network.http import RequestUtils
class MessageChain(ChainBase): 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 注册副作用 import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用
from app.application.image import ImageHelper
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.bangumi import BangumiChain from app.chain.bangumi import BangumiChain
from app.chain.douban import DoubanChain from app.chain.douban import DoubanChain
from app.chain.listenbrainz import ListenBrainzChain from app.chain.listenbrainz import ListenBrainzChain
from app.chain.tmdb import TmdbChain 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.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.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.media import normalize_media_source
from app.schemas.types import ( from app.schemas.types import (
MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING, MUSIC_ENTITY_RECORDING,
MediaSource, MediaSource,
MediaType, 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): 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 page in range(1, self.cache_max_pages + 1):
for method in recommend_methods: for method in recommend_methods:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
return return
if method in methods_finished: if method in methods_finished:
continue continue
@@ -277,7 +277,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
total_num = len(datas) total_num = len(datas)
for index, data in enumerate(datas, start=1): for index, data in enumerate(datas, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
return return
poster_path = data.get("poster_path") poster_path = data.get("poster_path")
if poster_path: if poster_path:
+15 -18
View File
@@ -7,47 +7,44 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory
from threading import Lock from threading import Lock
from typing import Any, Iterable, List, Optional, Tuple, Union 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 import ChainBase
from app.chain.lrclib import LrclibChain from app.chain.lrclib import LrclibChain
from app.chain.media import MediaChain
from app.chain.storage import StorageChain from app.chain.storage import StorageChain
from app.runtime.cache import cached
from app.domain.context import ( from app.domain.context import (
MediaInfo, MediaInfo,
MusicAlbumInfo, MusicAlbumInfo,
MusicInfo, MusicInfo,
MusicLyrics, MusicLyrics,
) )
from app.runtime.events import eventmanager, Event
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.application.configuration import ( from app.foundation.singleton import Singleton
get_chain_runtime_config_snapshot, from app.runtime.cache import cached
get_configured_system_config, from app.runtime.events import Event, eventmanager
)
from app.application.audio import AudioMetadataHelper
from app.runtime.log import logger 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 ( from app.schemas.types import (
MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING, MUSIC_ENTITY_RECORDING,
EventType, EventType,
MediaSource, MediaSource,
MediaType, MediaType,
ScrapingTarget,
ScrapingMetadata, ScrapingMetadata,
ScrapingPolicy, ScrapingPolicy,
ScrapingTarget,
SystemConfigKey, SystemConfigKey,
) )
from app.adapters.network.http import RequestUtils from app.schemas.workflow import FileItem
from app.schemas.media import resolve_media_identity from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.reload import ConfigReloadMixin
from app.foundation.singleton import Singleton
from app.chain.media import MediaChain
scraping_lock = Lock() scraping_lock = Lock()
+22 -25
View File
@@ -7,34 +7,34 @@ import time
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, as_completed, wait from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, as_completed, wait
from contextlib import aclosing from contextlib import aclosing
from datetime import datetime from datetime import datetime
from typing import AsyncIterator, Any, Awaitable, Callable, Dict, Iterable, Tuple from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterable, List, Optional, Tuple
from typing import List, Optional
from unicodedata import normalize 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 ( from app.application.configuration import (
get_chain_runtime_config_snapshot, get_chain_runtime_config_snapshot,
get_configured_system_config, 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 ( from app.application.search.state import (
SearchStateService, SearchStateService,
normalize_search_params, normalize_search_params,
stringify_sites, 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.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.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.mediaserver import NotExistMediaInfo
from app.schemas.types import ( from app.schemas.types import (
MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ALBUM,
@@ -44,9 +44,6 @@ from app.schemas.types import (
ProgressKey, ProgressKey,
SystemConfigKey, 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): class SearchChain(ChainBase):
@@ -1355,7 +1352,7 @@ class SearchChain(ChainBase):
logger.info(f"开始匹配结果 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}") logger.info(f"开始匹配结果 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
progress.update(value=51, text=f'开始匹配,总 {_total} 个资源 ...') progress.update(value=51, text=f'开始匹配,总 {_total} 个资源 ...')
for torrent in torrents: for torrent in torrents:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
_count += 1 _count += 1
progress.update(value=(_count / _total) * 96, progress.update(value=(_count / _total) * 96,
@@ -1709,7 +1706,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo), **self._media_recognize_kwargs(mediainfo),
) )
if not mediainfo: if not mediainfo:
logger.error(f'媒体信息识别失败!') logger.error('媒体信息识别失败!')
return [] return []
# 准备搜索参数 # 准备搜索参数
@@ -1802,7 +1799,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo), **self._media_recognize_kwargs(mediainfo),
) )
if not mediainfo: if not mediainfo:
logger.error(f'媒体信息识别失败!') logger.error('媒体信息识别失败!')
return [] return []
# 准备搜索参数 # 准备搜索参数
@@ -1885,7 +1882,7 @@ class SearchChain(ChainBase):
**self._media_recognize_kwargs(mediainfo), **self._media_recognize_kwargs(mediainfo),
) )
if not mediainfo: if not mediainfo:
logger.error(f'媒体信息识别失败!') logger.error('媒体信息识别失败!')
yield { yield {
"type": "error", "type": "error",
"success": False, "success": False,
@@ -2071,7 +2068,7 @@ class SearchChain(ChainBase):
match_subtitles = [] match_subtitles = []
logger.info(f"开始匹配字幕 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}") logger.info(f"开始匹配字幕 标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
for subtitle in subtitles: for subtitle in subtitles:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
subtitle_names = self.__build_subtitle_names(subtitle) subtitle_names = self.__build_subtitle_names(subtitle)
if not subtitle_names: if not subtitle_names:
@@ -2384,7 +2381,7 @@ class SearchChain(ChainBase):
try: try:
while pending_tasks: while pending_tasks:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
done_tasks, _ = wait(pending_tasks, return_when=FIRST_COMPLETED) done_tasks, _ = wait(pending_tasks, return_when=FIRST_COMPLETED)
for future in done_tasks: for future in done_tasks:
@@ -2460,7 +2457,7 @@ class SearchChain(ChainBase):
try: try:
while pending_tasks: while pending_tasks:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
done_tasks, _ = await asyncio.wait( done_tasks, _ = await asyncio.wait(
pending_tasks, pending_tasks,
+23 -23
View File
@@ -1,35 +1,35 @@
import base64 import base64
import re import re
from datetime import datetime 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 urllib.parse import urljoin
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from lxml import etree from lxml import etree
from app.chain import ChainBase from app.adapters.external.cookiecloud import CookieCloudHelper
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.network.browser import PlaywrightHelper from app.adapters.network.browser import PlaywrightHelper
from app.adapters.network.cloudflare import under_challenge from app.adapters.network.cloudflare import under_challenge
from app.application.security.cookie import CookieHelper from app.adapters.network.http import RequestUtils
from app.adapters.external.cookiecloud import CookieCloudHelper 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.messaging.site import SiteInteractionHandler
from app.application.rss import RssHelper from app.application.rss import RssHelper
from app.runtime.log import logger from app.application.security.cookie import CookieHelper
from app.schemas.notification import NotificationChannel from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.schemas.message import Message from app.chain import ChainBase
from app.schemas.site import SiteUserData from app.chain._interaction import InteractionChainMixin
from app.schemas.types import EventType, MessageType
from app.adapters.network.http import RequestUtils
from app.domain.site import SiteUtils
from app.domain import site as site_rules 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 size as size_tools
from app.foundation import url as url_tools from app.foundation import url as url_tools
from app.foundation.dom import DomUtils 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 Site = Any
@@ -83,7 +83,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)): re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)):
self.post_message(Message( self.post_message(Message(
mtype=MessageType.SiteMessage, mtype=MessageType.SiteMessage,
title=f"【站点分享率低预警】", title="【站点分享率低预警】",
text=f"站点 {site.get('name')} 分享率 {userdata.ratio},请注意!" text=f"站点 {site.get('name')} 分享率 {userdata.ratio},请注意!"
)) ))
return userdata return userdata
@@ -140,7 +140,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0}, data={"total": total_num, "finished": 0},
) )
for index, site in enumerate(sites, start=1): for index, site in enumerate(sites, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
return None return None
if progress_callback: if progress_callback:
progress_callback( progress_callback(
@@ -429,7 +429,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
update_count = add_count = fail_count = 0 update_count = add_count = fail_count = 0
for index, (domain, cookie) in enumerate(cookies.items(), start=1): 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同步") logger.info("系统正在停止,中断CookieCloud同步")
return False, "系统正在停止,同步被中断" return False, "系统正在停止,同步被中断"
if progress_callback: if progress_callback:
@@ -708,8 +708,8 @@ class SiteChain(InteractionChainMixin, ChainBase):
timeout=timeout) timeout=timeout)
if not public and not SiteUtils.is_logged_in(page_source): if not public and not SiteUtils.is_logged_in(page_source):
if under_challenge(page_source): if under_challenge(page_source):
return False, f"无法通过Cloudflare" return False, "无法通过Cloudflare"
return False, f"仿真登录失败,Cookie已失效!" return False, "仿真登录失败,Cookie已失效!"
else: else:
res = RequestUtils(cookies=site_cookie, res = RequestUtils(cookies=site_cookie,
ua=ua, ua=ua,
@@ -731,7 +731,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
elif res is not None: elif res is not None:
return False, f"错误:{res.status_code} {res.reason}" return False, f"错误:{res.status_code} {res.reason}"
else: else:
return False, f"无法打开网站!" return False, "无法打开网站!"
return True, "连接成功" return True, "连接成功"
def _interaction_handler(self) -> "SiteInteractionHandler": def _interaction_handler(self) -> "SiteInteractionHandler":
+3 -3
View File
@@ -1,10 +1,10 @@
from pathlib import Path 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.application.directory import DirectoryHelper
from app.chain import ChainBase
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.workflow import FileItem as _SchemaFileItem
class StorageChain(ChainBase): class StorageChain(ChainBase):
+87 -55
View File
@@ -5,35 +5,9 @@ import threading
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime 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.adapters.external.server import MoviePilotServerHelper
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.application.chain.data import ( from app.application.chain.data import (
get_chain_download_history_port, get_chain_download_history_port,
get_chain_site_port, get_chain_site_port,
@@ -43,30 +17,66 @@ from app.application.configuration import (
get_chain_runtime_config_snapshot, get_chain_runtime_config_snapshot,
get_configured_system_config, 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.mediaserver import MediaServerHelper
from app.application.subscription.write import add_subscribe, async_add_subscribe from app.application.messaging.message import MessageTemplateHelper
from app.application.subscription.complete import get_subscription_completion_scope from app.application.messaging.subscribe import SubscribeInteractionHandler
from app.application.subscription import priority as _priority 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 ( from app.application.subscription.delete import (
SubscribeDeletionActor, SubscribeDeletionActor,
get_sync_delete_subscribe_scope, 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.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.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.runtime.log import logger
from app.schemas.event import SubscribeEpisodesRefreshEventData from app.runtime.stop import runtime_stop_state
from app.schemas.event import SubscribeCompletionCheckEventData from app.schemas.event import SubscribeCompletionCheckEventData, SubscribeEpisodesRefreshEventData
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType, SystemConfigKey, NotificationChannel, MessageType, EventType, ChainEventType, \
ContentType
from app.schemas.media import normalize_media_source, resolve_media_identity 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"): if hasattr(_SchemaSubscribe, "model_fields"):
Subscribe = _SchemaSubscribe Subscribe = _SchemaSubscribe
@@ -191,6 +201,28 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
电影下载优先级 writer 单独维护 电影下载优先级 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 委托 # 交互处理器类注入,供 InteractionChainMixin 的 parse_callback 委托
_interaction_handler_type = SubscribeInteractionHandler _interaction_handler_type = SubscribeInteractionHandler
@@ -1179,7 +1211,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
try: try:
# 遍历订阅 # 遍历订阅
for index, subscribe in enumerate(subscribes, start=1): for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
processed_subscribes.append(subscribe) processed_subscribes.append(subscribe)
if progress_callback: if progress_callback:
@@ -1275,7 +1307,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
matched_contexts = [] matched_contexts = []
try: try:
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
torrent_meta = context.meta_info torrent_meta = context.meta_info
torrent_info = context.torrent_info torrent_info = context.torrent_info
@@ -1592,11 +1624,11 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
"""预识别待匹配资源,并保留原上下文供后续订阅复用。""" """预识别待匹配资源,并保留原上下文供后续订阅复用。"""
processed_torrents: Dict[str, List[Context]] = {} processed_torrents: Dict[str, List[Context]] = {}
for domain, contexts in torrents.items(): for domain, contexts in torrents.items():
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
processed_torrents[domain] = [] processed_torrents[domain] = []
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if context.torrent_info and getattr(context.torrent_info, "category", None) in ( if context.torrent_info and getattr(context.torrent_info, "category", None) in (
MediaType.MUSIC, MediaType.MUSIC,
@@ -1699,7 +1731,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
) )
try: try:
for index, subscribe in enumerate(subscribes, start=1): for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if progress_callback: if progress_callback:
progress_callback( progress_callback(
@@ -1772,13 +1804,13 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
systemconfig = _system_config() systemconfig = _system_config()
wordsmatcher = WordsMatcher() wordsmatcher = WordsMatcher()
for domain, contexts in processed_torrents.items(): for domain, contexts in processed_torrents.items():
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if domains and domain not in domains: if domains and domain not in domains:
continue continue
logger.debug(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...') logger.debug(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...')
for context in contexts: for context in contexts:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
# 提取信息 # 提取信息
_context = copy.copy(context) _context = copy.copy(context)
@@ -2046,7 +2078,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
) )
# 遍历订阅 # 遍历订阅
for index, subscribe in enumerate(subscribes, start=1): for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
logger.info(f'开始更新订阅元数据:{subscribe.name} ...') logger.info(f'开始更新订阅元数据:{subscribe.name} ...')
if progress_callback: if progress_callback:
@@ -2174,7 +2206,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
if progress_callback: if progress_callback:
progress_callback(value=100, text="未配置 Follow 订阅用户,跳过刷新") progress_callback(value=100, text="未配置 Follow 订阅用户,跳过刷新")
return return
logger.info(f'开始刷新follow用户分享订阅 ...') logger.info('开始刷新follow用户分享订阅 ...')
success_count = 0 success_count = 0
subscribeoper = get_chain_subscribe_port() subscribeoper = get_chain_subscribe_port()
share_subscribes = MoviePilotServerHelper.get_subscribe_shares() or [] share_subscribes = MoviePilotServerHelper.get_subscribe_shares() or []
@@ -2186,7 +2218,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0}, data={"total": total_num, "finished": 0},
) )
for index, share_sub in enumerate(share_subscribes, start=1): for index, share_sub in enumerate(share_subscribes, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if progress_callback: if progress_callback:
progress_callback( progress_callback(
@@ -2276,7 +2308,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
:param progress_callback: 定时服务进度更新回调 :param progress_callback: 定时服务进度更新回调
""" """
logger.info(f'开始预缓存订阅日历 ...') logger.info('开始预缓存订阅日历 ...')
subscribes = await get_chain_subscribe_port().async_list() subscribes = await get_chain_subscribe_port().async_list()
total_num = len(subscribes) total_num = len(subscribes)
if progress_callback: if progress_callback:
@@ -2286,7 +2318,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
data={"total": total_num, "finished": 0}, data={"total": total_num, "finished": 0},
) )
for index, subscribe in enumerate(subscribes, start=1): for index, subscribe in enumerate(subscribes, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if progress_callback: if progress_callback:
progress_callback( progress_callback(
@@ -2336,7 +2368,7 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
text=f"订阅日历({index}/{total_num})预缓存完成", text=f"订阅日历({index}/{total_num})预缓存完成",
data={"total": total_num, "finished": index}, data={"total": total_num, "finished": index},
) )
logger.info(f'订阅日历预缓存完成') logger.info('订阅日历预缓存完成')
if progress_callback: if progress_callback:
progress_callback(value=100, text="订阅日历预缓存完成") progress_callback(value=100, text="订阅日历预缓存完成")
+8 -8
View File
@@ -4,17 +4,17 @@ import re
import shutil import shutil
import uuid import uuid
from pathlib import Path 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.network.http import RequestUtils
from app.adapters.system.host import SystemUtils 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 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): class SystemChain(ChainBase):
@@ -33,7 +33,7 @@ class SystemChain(ChainBase):
self.post_message(Message( self.post_message(Message(
channel=channel, channel=channel,
source=source, source=source,
title=f"缓存清理完成!", title="缓存清理完成!",
userid=userid, userid=userid,
save_history=False)) save_history=False))
+4 -4
View File
@@ -1,11 +1,11 @@
import random 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.chain import ChainBase
from app.domain.context import MediaInfo 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 from app.schemas.types import MediaType
+20 -22
View File
@@ -1,27 +1,25 @@
import copy import copy
import re import re
import traceback 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.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config from app.application.configuration import get_configured_system_config
from app.application.rss import RssHelper 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.application.torrent import TorrentHelper
from app.runtime.log import logger from app.chain import ChainBase
from app.schemas.message import Message from app.chain.media import MediaChain
from app.schemas.types import SystemConfigKey, NotificationChannel, MessageType, MediaType
from app.schemas.media import resolve_media_identity
from app.domain import site as site_rules 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.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): class TorrentsChain(ChainBase):
@@ -50,13 +48,13 @@ class TorrentsChain(ChainBase):
""" """
self.post_message(Message( self.post_message(Message(
channel=channel, channel=channel,
title=f"开始刷新种子 ...", title="开始刷新种子 ...",
userid=userid, userid=userid,
save_history=False)) save_history=False))
self.refresh() self.refresh()
self.post_message(Message( self.post_message(Message(
channel=channel, channel=channel,
title=f"种子刷新完成!", title="种子刷新完成!",
userid=userid, userid=userid,
save_history=False)) 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._spider_file)
self.remove_cache(self._rss_file) self.remove_cache(self._rss_file)
self.remove_cache(self._music_spider_file) self.remove_cache(self._music_spider_file)
self.remove_cache(self._music_rss_file) self.remove_cache(self._music_rss_file)
logger.info(f'种子缓存数据清理完成') logger.info('种子缓存数据清理完成')
async def async_clear_torrents(self): 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._spider_file)
await self.async_remove_cache(self._rss_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_spider_file)
await self.async_remove_cache(self._music_rss_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, def browse(self, domain: str, keyword: Optional[str] = None, cat: Optional[str] = None,
page: Optional[int] = 0, page: Optional[int] = 0,
@@ -585,7 +583,7 @@ class TorrentsChain(ChainBase):
return domain return domain
logger.info(f'{indexer.get("name")}{len(torrents) + len(music_torrents)} 个新种子') logger.info(f'{indexer.get("name")}{len(torrents) + len(music_torrents)} 个新种子')
for torrent in torrents + music_torrents: for torrent in torrents + music_torrents:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if not torrent.enclosure: if not torrent.enclosure:
logger.warning(f"缺少种子链接,忽略处理: {torrent.title}") logger.warning(f"缺少种子链接,忽略处理: {torrent.title}")
@@ -692,7 +690,7 @@ class TorrentsChain(ChainBase):
) )
# 遍历站点缓存资源 # 遍历站点缓存资源
for index, indexer in enumerate(indexers, start=1): for index, indexer in enumerate(indexers, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if progress_callback: if progress_callback:
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 concurrent.futures import Future
from copy import deepcopy from copy import deepcopy
from pathlib import Path 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 ( from app.application.chain.data import (
get_chain_download_history_port, get_chain_download_history_port,
get_chain_transfer_history_port, get_chain_transfer_history_port,
get_chain_transfer_pending_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 DownloadHistory = Any
from app.application.configuration import get_configured_system_config from app.application.configuration import get_configured_system_config
from app.application.directory import DirectoryHelper from app.application.directory import DirectoryHelper
from app.application.formatting import FormatParser from app.application.formatting import FormatParser
from app.runtime.progress import ProgressHelper from app.application.history import (
from app.application.history import (add_transfer_fail, add_transfer_success, add_transfer_fail,
clear_transfer_failures, describe_history_gate, add_transfer_success,
evaluate_history_gate, is_skip_action, clear_transfer_failures,
record_transfer_failure) describe_history_gate,
from app.application.outbox import TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC evaluate_history_gate,
from app.runtime.log import logger is_skip_action,
from app.schemas.event import StorageOperSelectionEventData record_transfer_failure,
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.runtime.reload import ConfigReloadMixin from app.application.outbox import TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC
from app.application.transfer import ( from app.application.transfer import (
FailedRetryScheduler, FailedRetryScheduler,
JobManager, JobManager,
@@ -70,13 +53,40 @@ from app.application.transfer import (
build_transfer_failure_group_key, build_transfer_failure_group_key,
job_lock, job_lock,
) )
from app.chain._transfer import (EpisodeFormatMixin, FailedRetryMixin, from app.chain._transfer import (
FileFilterMixin, FileKeyMixin, EpisodeFormatMixin,
HistoryMatchMixin, ManualHistoryMixin, FailedRetryMixin,
ScrapeBatchMixin) FileFilterMixin,
from app.schemas.media import resolve_media_identity FileKeyMixin,
from app.foundation.singleton import Singleton HistoryMatchMixin,
ManualHistoryMixin,
ScrapeBatchMixin,
)
from app.domain import episode as episode_rules 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() 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 清理入口。 # worker 在构造期启动;若中途失败,单例仍需先发布给 lifespan 清理入口。
_retain_failed_singleton = True _retain_failed_singleton = True
@@ -1148,7 +1176,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
:param stop_event: 当前 worker 代专属停止信号热更新后不会被重新清除 :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: try:
item: TransferQueue = self._queue.get( item: TransferQueue = self._queue.get(
block=True, timeout=self._transfer_interval block=True, timeout=self._transfer_interval
@@ -1156,10 +1184,10 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
if item is self._QUEUE_STOP_SENTINEL: if item is self._QUEUE_STOP_SENTINEL:
self._queue.task_done() self._queue.task_done()
self.__settle_transfer_progress_if_idle() 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 break
continue 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 竞态时,把尚未处理的任务放回队列;其 # 关闭信号与 queue.get 竞态时,把尚未处理的任务放回队列;其
# TransferPending 登记保持不变,供同进程重启 worker 或下次启动回放。 # TransferPending 登记保持不变,供同进程重启 worker 或下次启动回放。
self._queue.put(item) self._queue.put(item)
@@ -1606,7 +1634,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
try: try:
total_num = len(torrents) total_num = len(torrents)
for index, torrent in enumerate(torrents, start=1): for index, torrent in enumerate(torrents, start=1):
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if progress_callback: if progress_callback:
torrent_name = ( torrent_name = (
@@ -1732,7 +1760,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
`predicate` `None`则默认保留所有项 `predicate` `None`则默认保留所有项
:param verify_file_exists: 验证目录或文件是否存在默认值为 `True` :param verify_file_exists: 验证目录或文件是否存在默认值为 `True`
""" """
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
raise OperationInterrupted() raise OperationInterrupted()
storagechain = StorageChain() storagechain = StorageChain()
@@ -2536,7 +2564,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
skipped_torrents = set() skipped_torrents = set()
try: try:
for file_item, bluray_dir in file_items: for file_item, bluray_dir in file_items:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
raise OperationInterrupted() raise OperationInterrupted()
if continue_callback and not continue_callback(): if continue_callback and not continue_callback():
raise OperationInterrupted() raise OperationInterrupted()
@@ -2762,7 +2790,7 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
progress.update(value=0, text=__process_msg) progress.update(value=0, text=__process_msg)
try: try:
for transfer_task in transfer_tasks: for transfer_task in transfer_tasks:
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
break break
if continue_callback and not continue_callback(): if continue_callback and not continue_callback():
break break
+5 -6
View File
@@ -2,14 +2,13 @@ import secrets
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Literal, Optional, Tuple, Union 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.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.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 = "用户名、密码或验证码错误" PASSWORD_INVALID_CREDENTIALS_MESSAGE = "用户名、密码或验证码错误"
User = Any User = Any
+8 -12
View File
@@ -13,19 +13,15 @@ from typing import Any, Callable, List, Optional, Tuple
from pydantic import BaseModel 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.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.execution import OwnedThreadPoolExecutor
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.workflow import ActionContext from app.runtime.stop import runtime_stop_state
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.schemas.types import EventType from app.schemas.types import EventType
from app.schemas.workflow import Action, ActionContext, ActionExecution, ActionFlow, ActionResult
ARTIFACT_FIELDS = {"torrents", "medias", "fileitems", "downloads", "sites", "subscribes"} ARTIFACT_FIELDS = {"torrents", "medias", "fileitems", "downloads", "sites", "subscribes"}
DEFAULT_WORKFLOW_MAX_WORKERS = 4 DEFAULT_WORKFLOW_MAX_WORKERS = 4
@@ -113,7 +109,7 @@ class WorkflowCancelToken:
""" """
return bool( return bool(
(self.stop_event and self.stop_event.is_set()) (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._registered_execution = callable(register)
self._admission_state = "admitted" self._admission_state = "admitted"
# 只有获得执行准入后才能清除历史单工作流停止标记。 # 只有获得执行准入后才能清除历史单工作流停止标记。
global_vars.workflow_resume(self.workflow.id) runtime_stop_state.resume_workflow(self.workflow.id)
return True return True
def request_stop(self) -> None: def request_stop(self) -> None:
@@ -285,7 +281,7 @@ class WorkflowExecutor:
"""判断本次执行或全局工作流是否已收到停止请求。""" """判断本次执行或全局工作流是否已收到停止请求。"""
return bool( return bool(
self._stop_event.is_set() 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: def get_workflow_max_workers(self) -> int:
+7 -6
View File
@@ -26,18 +26,18 @@ def _prepare_direct_execution_import_path() -> None:
_prepare_direct_execution_import_path() _prepare_direct_execution_import_path()
import setproctitle
import signal import signal
import threading import threading
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import setproctitle
import uvicorn as uvicorn import uvicorn as uvicorn
from PIL import Image from PIL import Image
from uvicorn import Config from uvicorn import Config
from app.adapters.system.stdio import configure_rotating_stdio
from app.adapters.system.host import SystemUtils from app.adapters.system.host import SystemUtils
from app.adapters.system.stdio import configure_rotating_stdio
stdio_log_file = os.getenv("MOVIEPILOT_STDIO_LOG_FILE") stdio_log_file = os.getenv("MOVIEPILOT_STDIO_LOG_FILE")
if stdio_log_file: if stdio_log_file:
@@ -55,8 +55,9 @@ elif SystemUtils.is_frozen():
sys.stderr = open(os.devnull, 'w') sys.stderr = open(os.devnull, 'w')
from app.factory import app from app.factory import app
from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import global_vars
from app.runtime.stop import runtime_stop_state
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.topology import ( from app.runtime.topology import (
@@ -71,7 +72,7 @@ class MoviePilotServer(uvicorn.Server):
"""在 Uvicorn 开始优雅退出前发布应用协作停止标志""" """在 Uvicorn 开始优雅退出前发布应用协作停止标志"""
def handle_exit(self, sig, frame) -> None: def handle_exit(self, sig, frame) -> None:
global_vars.stop_system() getattr(global_vars, "stop_system")()
super().handle_exit(sig, frame) super().handle_exit(sig, frame)
@@ -92,7 +93,7 @@ def create_server() -> MoviePilotServer:
) )
) )
# 数据库准备阶段收到的信号早于 Server 物化,创建后必须继承既有停止意图。 # 数据库准备阶段收到的信号早于 Server 物化,创建后必须继承既有停止意图。
if global_vars.is_system_stopped: if runtime_stop_state.is_system_stopped:
server.should_exit = True server.should_exit = True
return server return server
@@ -124,7 +125,7 @@ def run_api_server() -> None:
def request_shutdown() -> None: def request_shutdown() -> None:
"""发布协作停止标志并请求 Uvicorn 退出""" """发布协作停止标志并请求 Uvicorn 退出"""
global_vars.stop_system() getattr(global_vars, "stop_system")()
if Server is not None: if Server is not None:
Server.should_exit = True Server.should_exit = True
+6 -6
View File
@@ -1,17 +1,17 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from typing import Optional, List, Dict, Tuple, Callable, Union from typing import Callable, Dict, List, Optional, Tuple, Union
from tqdm import tqdm from tqdm import tqdm
from app.application.storage import StorageHelper
from app.foundation.crypto import HashUtils
from app.runtime.log import logger
from app.runtime.progress import ProgressHelper
from app.schemas.exception import StorageQueryError
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.system import StorageConf as _SchemaStorageConf from app.schemas.system import StorageConf as _SchemaStorageConf
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.progress import ProgressHelper
from app.application.storage import StorageHelper
from app.runtime.log import logger
from app.schemas.exception import StorageQueryError
from app.foundation.crypto import HashUtils
def transfer_process(path: str) -> Callable[[int | float], None]: def transfer_process(path: str) -> Callable[[int | float], None]:
+8 -8
View File
@@ -8,19 +8,19 @@ from typing import List, Optional, Tuple, Union
import requests import requests
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import global_vars
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.log import logger from app.adapters.network.http import RequestUtils
from app.foundation import temporal as time_tools
from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.schemas.exception import StorageQueryError from app.schemas.exception import StorageQueryError
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.adapters.network.http import RequestUtils
from app.foundation.singleton import WeakSingleton
from app.foundation import temporal as time_tools
lock = threading.Lock() lock = threading.Lock()
@@ -645,7 +645,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
uploaded_size = 0 uploaded_size = 0
with open(local_path, "rb") as f: with open(local_path, "rb") as f:
for part_info in part_info_list: for part_info in part_info_list:
if global_vars.is_transfer_stopped(local_path.as_posix()): if runtime_stop_state.consume_transfer_stop(local_path.as_posix()):
logger.info(f"【阿里云盘】{target_name} 上传已取消!") logger.info(f"【阿里云盘】{target_name} 上传已取消!")
return None return None
@@ -780,7 +780,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
downloaded_size = 0 downloaded_size = 0
with open(local_path, "wb") as f: with open(local_path, "wb") as f:
for chunk in r.iter_content(chunk_size=self.chunk_size): for chunk in r.iter_content(chunk_size=self.chunk_size):
if global_vars.is_transfer_stopped(fileitem.path): if runtime_stop_state.consume_transfer_stop(fileitem.path):
logger.info(f"【阿里云盘】{fileitem.path} 下载已取消!") logger.info(f"【阿里云盘】{fileitem.path} 下载已取消!")
return None return None
if chunk: if chunk:
+10 -11
View File
@@ -3,23 +3,22 @@ import json
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, List from typing import List, Optional
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.cache import cached from app.runtime.cache import cached
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import global_vars from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.log import logger
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.schemas.exception import OperationInterrupted, StorageQueryError
from app.schemas.types import StorageSchema
from app.adapters.network.http import RequestUtils from app.adapters.network.http import RequestUtils
from app.foundation.singleton import WeakSingleton from app.foundation.singleton import WeakSingleton
from app.foundation.url import UrlUtils from app.foundation.url import UrlUtils
from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.schemas.exception import OperationInterrupted, StorageQueryError
from app.schemas.types import StorageSchema
# OpenList/AList 在 per_page<=0 时会退回后端默认 200,显式指定最大页大小避免大目录被截断。 # OpenList/AList 在 per_page<=0 时会退回后端默认 200,显式指定最大页大小避免大目录被截断。
OPENLIST_MAX_LIST_PAGE_SIZE = 500 OPENLIST_MAX_LIST_PAGE_SIZE = 500
@@ -703,7 +702,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
r.raise_for_status() r.raise_for_status()
with open(local_path, "wb") as f: with open(local_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192): for chunk in r.iter_content(chunk_size=8192):
if global_vars.is_transfer_stopped(fileitem.path): if runtime_stop_state.consume_transfer_stop(fileitem.path):
logger.info(f"【OpenList】{fileitem.path} 下载已取消!") logger.info(f"【OpenList】{fileitem.path} 下载已取消!")
return None return None
f.write(chunk) f.write(chunk)
@@ -760,7 +759,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
return self.file_size return self.file_size
def read(self, size=-1): def read(self, size=-1):
if global_vars.is_transfer_stopped(path.as_posix()): if runtime_stop_state.consume_transfer_stop(path.as_posix()):
logger.info(f"【OpenList】{path} 上传已取消!") logger.info(f"【OpenList】{path} 上传已取消!")
raise OperationInterrupted(f"Upload cancelled: {path}") raise OperationInterrupted(f"Upload cancelled: {path}")
chunk = self.file.read(size) chunk = self.file.read(size)
+8 -8
View File
@@ -2,21 +2,21 @@ import os
import shutil import shutil
import time import time
from pathlib import Path from pathlib import Path
from typing import Optional, List from typing import List, Optional
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.application.directory import DirectoryHelper
from app.runtime.log import logger
from app.adapters.system.fsproxy import fsproxy from app.adapters.system.fsproxy import fsproxy
from app.adapters.system.host import SystemUtils
from app.application.directory import DirectoryHelper
from app.modules.filemanager.storages import StorageBase, transfer_process from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.schemas.exception import StorageQueryError from app.schemas.exception import StorageQueryError
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.adapters.system.host import SystemUtils
class LocalStorage(StorageBase): class LocalStorage(StorageBase):
@@ -301,7 +301,7 @@ class LocalStorage(StorageBase):
copied = fsproxy.copy( copied = fsproxy.copy(
src, partial, src, partial,
progress_cb=progress_callback, progress_cb=progress_callback,
cancel_cb=lambda: global_vars.is_transfer_stopped(src.as_posix()), cancel_cb=lambda: runtime_stop_state.consume_transfer_stop(src.as_posix()),
chunk_size=self.chunk_size, chunk_size=self.chunk_size,
) )
if not copied: if not copied:
@@ -352,7 +352,7 @@ class LocalStorage(StorageBase):
try: try:
with open(src, "rb") as fsrc, open(dest, "wb") as fdst: with open(src, "rb") as fsrc, open(dest, "wb") as fdst:
while True: while True:
if global_vars.is_transfer_stopped(src.as_posix()): if runtime_stop_state.consume_transfer_stop(src.as_posix()):
logger.info(f"【本地】{src} 复制已取消!") logger.info(f"【本地】{src} 复制已取消!")
return False return False
buf = fsrc.read(self.chunk_size) buf = fsrc.read(self.chunk_size)
+5 -5
View File
@@ -4,19 +4,19 @@ import threading
import time import time
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union from typing import List, Optional, Union
from app.runtime.settings import RuntimeSettingsCompat
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.log import logger from app.adapters.system.host import SystemUtils
from app.foundation import temporal as time_tools
from app.modules.filemanager.storages import StorageBase, transfer_process from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.schemas.exception import StorageQueryError from app.schemas.exception import StorageQueryError
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.foundation import temporal as time_tools
from app.adapters.system.host import SystemUtils
_MAX_FOLDER_LOCKS = 4096 _MAX_FOLDER_LOCKS = 4096
_folder_locks: OrderedDict[str, threading.Lock] = OrderedDict() _folder_locks: OrderedDict[str, threading.Lock] = OrderedDict()
+7 -7
View File
@@ -7,22 +7,22 @@ from typing import List, Optional, Union
import smbclient import smbclient
from smbclient import ClientConfig, register_session, reset_connection_cache from smbclient import ClientConfig, register_session, reset_connection_cache
from smbprotocol.exceptions import ( from smbprotocol.exceptions import (
SMBAuthenticationError,
SMBException, SMBException,
SMBResponseException, SMBResponseException,
SMBAuthenticationError,
) )
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import global_vars
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.log import logger from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.schemas.exception import StorageQueryError from app.schemas.exception import StorageQueryError
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.foundation.singleton import WeakSingleton
lock = threading.Lock() lock = threading.Lock()
@@ -572,7 +572,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
with open(local_path, "wb") as dst_file: with open(local_path, "wb") as dst_file:
downloaded_size = 0 downloaded_size = 0
while True: while True:
if global_vars.is_transfer_stopped(fileitem.path): if runtime_stop_state.consume_transfer_stop(fileitem.path):
logger.info(f"【SMB】{fileitem.path} 下载已取消!") logger.info(f"【SMB】{fileitem.path} 下载已取消!")
return None return None
chunk = src_file.read(self.chunk_size) chunk = src_file.read(self.chunk_size)
@@ -622,7 +622,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
with smbclient.open_file(smb_path, mode="wb") as dst_file: with smbclient.open_file(smb_path, mode="wb") as dst_file:
uploaded_size = 0 uploaded_size = 0
while True: while True:
if global_vars.is_transfer_stopped(path.as_posix()): if runtime_stop_state.consume_transfer_stop(path.as_posix()):
logger.info(f"【SMB】{path} 上传已取消!") logger.info(f"【SMB】{path} 上传已取消!")
return None return None
chunk = src_file.read(self.chunk_size) chunk = src_file.read(self.chunk_size)
+11 -12
View File
@@ -1,31 +1,30 @@
import base64 import base64
import secrets import secrets
import time import time
from hashlib import sha256
from pathlib import Path from pathlib import Path
from threading import Lock from threading import Lock
from typing import List, Optional, Tuple, Union from typing import List, Optional, Tuple, Union
from hashlib import sha256
import oss2
import httpx import httpx
import oss2
from cryptography.hazmat.primitives import hashes
from oss2 import SizedFileAdapter, determine_part_size from oss2 import SizedFileAdapter, determine_part_size
from oss2.models import PartInfo from oss2.models import PartInfo
from cryptography.hazmat.primitives import hashes
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.stop import runtime_stop_state
from app.schemas.file import StorageUsage as _SchemaStorageUsage from app.schemas.file import StorageUsage as _SchemaStorageUsage
from app.schemas.workflow import FileItem as _SchemaFileItem from app.schemas.workflow import FileItem as _SchemaFileItem
from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import global_vars
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.log import logger from app.foundation import size as size_tools
from app.foundation.singleton import WeakSingleton
from app.modules.filemanager.storages import StorageBase, transfer_process from app.modules.filemanager.storages import StorageBase, transfer_process
from app.runtime.log import logger
from app.runtime.rate import QpsRateLimiter, RateStats
from app.schemas.exception import StorageQueryError from app.schemas.exception import StorageQueryError
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.foundation.singleton import WeakSingleton
from app.foundation import size as size_tools
from app.runtime.rate import QpsRateLimiter, RateStats
lock = Lock() lock = Lock()
@@ -778,7 +777,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
part_number = 1 part_number = 1
offset = 0 offset = 0
while offset < file_size: while offset < file_size:
if global_vars.is_transfer_stopped(local_path.as_posix()): if runtime_stop_state.consume_transfer_stop(local_path.as_posix()):
logger.info(f"【115】{local_path} 上传已取消!") logger.info(f"【115】{local_path} 上传已取消!")
return None return None
num_to_upload = min(part_size, file_size - offset) num_to_upload = min(part_size, file_size - offset)
@@ -929,7 +928,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
with open(local_path, "wb") as f: with open(local_path, "wb") as f:
for chunk in r.iter_bytes(chunk_size=self.chunk_size): for chunk in r.iter_bytes(chunk_size=self.chunk_size):
if global_vars.is_transfer_stopped(fileitem.path): if runtime_stop_state.consume_transfer_stop(fileitem.path):
logger.info(f"【115】{fileitem.path} 下载已取消!") logger.info(f"【115】{fileitem.path} 下载已取消!")
r.close() r.close()
return None return None
+30 -28
View File
@@ -9,31 +9,32 @@ import sys
import threading import threading
from asyncio import AbstractEventLoop from asyncio import AbstractEventLoop
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_origin, get_args from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
from urllib.parse import quote, urlencode, urlparse from urllib.parse import quote, urlencode, urlparse
from dotenv import set_key, unset_key from dotenv import set_key, unset_key
from pydantic import BaseModel, Field, ConfigDict, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from app.foundation.environment import is_free_threaded_runtime
from app.runtime.log import (
LogConfigModel,
configure_log_settings,
configure_log_writer,
logger,
log_settings,
NonBlockingFileHandler,
)
from app.schemas.types import MediaType
from app.foundation.environment import ( from app.foundation.environment import (
cpu_arch, cpu_arch,
get_env_path, get_env_path,
is_docker, is_docker,
is_free_threaded_runtime,
is_frozen, is_frozen,
) )
from app.foundation.url import UrlUtils from app.foundation.url import UrlUtils
from app.runtime.log import (
LogConfigModel,
NonBlockingFileHandler,
configure_log_settings,
configure_log_writer,
log_settings,
logger,
)
from app.runtime.stop import runtime_stop_state
from app.runtime.version import get_app_version from app.runtime.version import get_app_version
from app.schemas.types import MediaType
class SystemConfModel(BaseModel): class SystemConfModel(BaseModel):
@@ -1374,7 +1375,6 @@ class GlobalVar(object):
""" """
# 系统停止事件 # 系统停止事件
STOP_EVENT: threading.Event = threading.Event()
# webpush订阅 # webpush订阅
SUBSCRIPTIONS: List[dict] = [] SUBSCRIPTIONS: List[dict] = []
# webpush订阅读写锁 # webpush订阅读写锁
@@ -1391,18 +1391,28 @@ class GlobalVar(object):
self._event_loop_owners: dict[object, AbstractEventLoop] = {} self._event_loop_owners: dict[object, AbstractEventLoop] = {}
self._event_loop_owner_lock = threading.Lock() self._event_loop_owner_lock = threading.Lock()
@property
def STOP_EVENT(self) -> threading.Event:
"""兼容旧代码读取进程停止事件。"""
return runtime_stop_state.system_event
@STOP_EVENT.setter
def STOP_EVENT(self, event: threading.Event) -> None:
"""兼容旧测试替换事件,同时保持新 StopState 为唯一状态源。"""
runtime_stop_state.replace_system_event(event)
def stop_system(self): def stop_system(self):
""" """
停止系统 停止系统
""" """
self.STOP_EVENT.set() runtime_stop_state.stop_system()
@property @property
def is_system_stopped(self): def is_system_stopped(self):
""" """
是否停止 是否停止
""" """
return self.STOP_EVENT.is_set() return runtime_stop_state.is_system_stopped
def get_subscriptions(self): def get_subscriptions(self):
""" """
@@ -1444,39 +1454,31 @@ class GlobalVar(object):
""" """
停止工作流 停止工作流
""" """
if workflow_id not in self.EMERGENCY_STOP_WORKFLOWS: runtime_stop_state.stop_workflow(workflow_id)
self.EMERGENCY_STOP_WORKFLOWS.append(workflow_id)
def workflow_resume(self, workflow_id: int): def workflow_resume(self, workflow_id: int):
""" """
恢复工作流 恢复工作流
""" """
if workflow_id in self.EMERGENCY_STOP_WORKFLOWS: runtime_stop_state.resume_workflow(workflow_id)
self.EMERGENCY_STOP_WORKFLOWS.remove(workflow_id)
def is_workflow_stopped(self, workflow_id: int) -> bool: def is_workflow_stopped(self, workflow_id: int) -> bool:
""" """
是否停止工作流 是否停止工作流
""" """
return self.is_system_stopped or workflow_id in self.EMERGENCY_STOP_WORKFLOWS return runtime_stop_state.is_workflow_stopped(workflow_id)
def stop_transfer(self, path: str): def stop_transfer(self, path: str):
""" """
停止文件整理 停止文件整理
""" """
if path not in self.EMERGENCY_STOP_TRANSFER: runtime_stop_state.stop_transfer(path)
self.EMERGENCY_STOP_TRANSFER.append(path)
def is_transfer_stopped(self, path: str) -> bool: def is_transfer_stopped(self, path: str) -> bool:
""" """
是否停止文件整理 是否停止文件整理
""" """
if self.is_system_stopped: return runtime_stop_state.consume_transfer_stop(path)
return True
if path in self.EMERGENCY_STOP_TRANSFER:
self.EMERGENCY_STOP_TRANSFER.remove(path)
return True
return False
@property @property
def loop(self) -> AbstractEventLoop: def loop(self) -> AbstractEventLoop:
+112
View File
@@ -0,0 +1,112 @@
"""进程停止与细粒度取消信号契约。"""
from __future__ import annotations
import threading
from typing import Protocol
class StopState(Protocol):
"""向运行时消费者暴露系统、工作流和整理任务的停止状态。"""
@property
def is_system_stopped(self) -> bool:
"""返回系统是否已经进入停止阶段。"""
...
def stop_system(self) -> None:
"""发布不可逆的当前进程停止信号。"""
...
def stop_workflow(self, workflow_id: int) -> None:
"""请求停止指定工作流。"""
...
def resume_workflow(self, workflow_id: int) -> None:
"""清除指定工作流的停止请求。"""
...
def is_workflow_stopped(self, workflow_id: int) -> bool:
"""返回系统或指定工作流是否已经停止。"""
...
def stop_transfer(self, path: str) -> None:
"""登记指定源路径的一次性整理停止请求。"""
...
def consume_transfer_stop(self, path: str) -> bool:
"""消费指定路径的停止请求;系统停止时始终返回真。"""
...
class ProcessStopState:
"""线程安全地持有当前进程的停止与细粒度取消状态。"""
def __init__(self) -> None:
self._system_event = threading.Event()
self._workflow_ids: set[int] = set()
self._transfer_paths: set[str] = set()
self._lock = threading.Lock()
@property
def system_event(self) -> threading.Event:
"""返回兼容入口使用的系统停止事件。"""
return self._system_event
def replace_system_event(self, event: threading.Event) -> None:
"""替换系统事件,仅供旧 ABI 与隔离测试继续使用。"""
self._system_event = event
@property
def is_system_stopped(self) -> bool:
"""返回系统是否已经进入停止阶段。"""
return self._system_event.is_set()
def stop_system(self) -> None:
"""发布不可逆的当前进程停止信号。"""
self._system_event.set()
def stop_workflow(self, workflow_id: int) -> None:
"""请求停止指定工作流。"""
with self._lock:
self._workflow_ids.add(workflow_id)
def resume_workflow(self, workflow_id: int) -> None:
"""清除指定工作流的停止请求。"""
with self._lock:
self._workflow_ids.discard(workflow_id)
def is_workflow_stopped(self, workflow_id: int) -> bool:
"""返回系统或指定工作流是否已经停止。"""
if self.is_system_stopped:
return True
with self._lock:
return workflow_id in self._workflow_ids
def stop_transfer(self, path: str) -> None:
"""登记指定源路径的一次性整理停止请求。"""
with self._lock:
self._transfer_paths.add(path)
def consume_transfer_stop(self, path: str) -> bool:
"""消费指定路径的停止请求;系统停止时始终返回真。"""
if self.is_system_stopped:
return True
with self._lock:
if path not in self._transfer_paths:
return False
self._transfer_paths.remove(path)
return True
def workflow_stop_ids(self) -> list[int]:
"""返回旧诊断入口需要的工作流停止快照。"""
with self._lock:
return list(self._workflow_ids)
def transfer_stop_paths(self) -> list[str]:
"""返回旧诊断入口需要的整理停止快照。"""
with self._lock:
return list(self._transfer_paths)
runtime_stop_state = ProcessStopState()
+34 -33
View File
@@ -9,16 +9,30 @@ import time
import traceback import traceback
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Callable, Optional, Dict, Any, List from typing import Any, Callable, Dict, List, Optional
import pytz import pytz
from apscheduler.executors.pool import ThreadPoolExecutor from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.jobstores.base import JobLookupError from apscheduler.jobstores.base import JobLookupError
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from app.schemas.dashboard import ScheduleInfo as _SchemaScheduleInfo
from app.schemas.dashboard import ScheduleProgress as _SchemaScheduleProgress from app.adapters.external.server import MoviePilotServerHelper
from app.schemas.system import MediaServerConf as _SchemaMediaServerConf from app.adapters.system.update import system_update_manager
from app.application.agentdata import get_agent_task_port
from app.application.configuration import (
SchedulerRuntimeConfig,
get_configured_system_config,
get_scheduler_runtime_config,
)
from app.application.database import get_database_governance
from app.application.image import WallpaperHelper
from app.application.mediaserver import get_mediaserver_configs
from app.application.messaging.message import MessageHelper
from app.application.outbox import dispatch_pending_outbox
from app.application.plugin.routes import register_plugin_api
from app.application.plugin.runtime import get_plugin_manager
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.chain.recommend import RecommendChain from app.chain.recommend import RecommendChain
@@ -26,36 +40,23 @@ from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain from app.chain.transfer import TransferChain
from app.chain.workflow import WorkflowChain from app.chain.workflow import WorkflowChain
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.application.agentdata import get_agent_task_port
from app.application.database import get_database_governance
from app.application.outbox import dispatch_pending_outbox
from app.application.plugin.runtime import get_plugin_manager
from app.application.plugin.routes import register_plugin_api
from app.application.configuration import (
SchedulerRuntimeConfig,
get_configured_system_config,
get_scheduler_runtime_config,
)
from app.application.image import WallpaperHelper
from app.application.mediaserver import get_mediaserver_configs
from app.application.messaging.message import MessageHelper
from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.system.update import system_update_manager
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.runtime.log import logger
from app.schemas.message import Message
from app.schemas.message import MessageType
from app.schemas.workflow import Workflow
from app.schemas.types import EventType, SystemConfigKey
from app.runtime.gc import get_memory_usage
from app.runtime.reload import ConfigReloadMixin
from app.foundation.singleton import SingletonClass from app.foundation.singleton import SingletonClass
from app.runtime.scheduling import TimerUtils from app.runtime.config import global_vars
from app.runtime.correlation import call_with_correlation, get_correlation_id from app.runtime.correlation import call_with_correlation, get_correlation_id
from app.runtime.events import Event, eventmanager
from app.runtime.gc import get_memory_usage
from app.runtime.log import logger
from app.runtime.observability import record_metric from app.runtime.observability import record_metric
from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.runtime.reload import ConfigReloadMixin
from app.runtime.scheduling import TimerUtils
from app.runtime.stop import runtime_stop_state
from app.schemas.dashboard import ScheduleInfo as _SchemaScheduleInfo
from app.schemas.dashboard import ScheduleProgress as _SchemaScheduleProgress
from app.schemas.message import Message, MessageType
from app.schemas.system import MediaServerConf as _SchemaMediaServerConf
from app.schemas.types import EventType, SystemConfigKey
from app.schemas.workflow import Workflow
lock = threading.Lock() lock = threading.Lock()
SCHEDULER_PROGRESS_PREFIX = "scheduler" SCHEDULER_PROGRESS_PREFIX = "scheduler"
@@ -1966,7 +1967,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
"""停止旧计划的提交入口,保留已开始任务直到其自然完成。""" """停止旧计划的提交入口,保留已开始任务直到其自然完成。"""
with self._lock: with self._lock:
if ( if (
global_vars.is_system_stopped runtime_stop_state.is_system_stopped
or self._lifecycle_state in {"stopping", "reloading"} or self._lifecycle_state in {"stopping", "reloading"}
): ):
return False, None return False, None
@@ -2066,7 +2067,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
if self._auth_count > __max_try__: if self._auth_count > __max_try__:
if not self._auth_message: if not self._auth_message:
SchedulerChain().messagehelper.put( SchedulerChain().messagehelper.put(
title=f"用户认证失败", title="用户认证失败",
message="用户认证失败次数过多,将不再尝试认证!", message="用户认证失败次数过多,将不再尝试认证!",
role="system", role="system",
) )
+3 -4
View File
@@ -4,15 +4,13 @@ from collections.abc import AsyncGenerator, Callable, Generator
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Protocol from typing import Protocol
from app.runtime.tasks import TaskRegistry from app.application.configuration import RuntimeConfiguration, RuntimeSettingsService
from app.application.messaging.chat import ( from app.application.messaging.chat import (
AsyncAgentChatRepository,
AgentChatPersistenceService, AgentChatPersistenceService,
AsyncAgentChatRepository,
AsyncUnitOfWork, AsyncUnitOfWork,
) )
from app.application.outbox import AsyncOutboxTransaction from app.application.outbox import AsyncOutboxTransaction
from app.application.configuration import RuntimeConfiguration, RuntimeSettingsService
from app.application.subscription.delete import SubscribeDeletionRepository from app.application.subscription.delete import SubscribeDeletionRepository
from app.application.subscription.identity import SubscribeIdentityDeletionRepository from app.application.subscription.identity import SubscribeIdentityDeletionRepository
from app.application.subscription.mutation import ( from app.application.subscription.mutation import (
@@ -20,6 +18,7 @@ from app.application.subscription.mutation import (
SubscriptionMutationRepository, SubscriptionMutationRepository,
) )
from app.application.workflow import WorkflowCachePort from app.application.workflow import WorkflowCachePort
from app.runtime.tasks import TaskRegistry
class AgentChatRepositoryFactory(Protocol): class AgentChatRepositoryFactory(Protocol):
+9 -4
View File
@@ -1,15 +1,19 @@
from typing import Any from typing import Any
from app.agent.llm.gateway import register_llm_provider_runtime
from app.agent.runtime_loader import ( from app.agent.runtime_loader import (
activate_agent_service, activate_agent_service,
begin_agent_shutdown, begin_agent_shutdown,
close_materialized_terminal_sessions, close_materialized_terminal_sessions,
get_agent_manager as get_runtime_agent_manager,
get_running_agent_manager as get_runtime_running_agent_manager,
is_tool_factory_materialized, is_tool_factory_materialized,
reconcile_agent_service, reconcile_agent_service,
) )
from app.agent.llm.gateway import register_llm_provider_runtime from app.agent.runtime_loader import (
get_agent_manager as get_runtime_agent_manager,
)
from app.agent.runtime_loader import (
get_running_agent_manager as get_runtime_running_agent_manager,
)
from app.application.agent import register_agent_service_providers from app.application.agent import register_agent_service_providers
from app.application.messaging.skill import register_skill_catalog_provider from app.application.messaging.skill import register_skill_catalog_provider
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
@@ -19,7 +23,6 @@ from app.runtime.events import Event, eventmanager
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.types import EventType from app.schemas.types import EventType
AGENT_BLOCKING_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS = 10.0 AGENT_BLOCKING_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS = 10.0
@@ -226,6 +229,8 @@ async def stop_agent() -> bool:
if is_tool_factory_materialized(): if is_tool_factory_materialized():
from app.agent.tools.base import ( from app.agent.tools.base import (
begin_blocking_executor_shutdown, begin_blocking_executor_shutdown,
)
from app.agent.tools.base import (
close_blocking_executors as close_executors, close_blocking_executors as close_executors,
) )
+1 -1
View File
@@ -1,6 +1,6 @@
import traceback
from collections.abc import Callable from collections.abc import Callable
from configparser import ConfigParser as _ConfigParser from configparser import ConfigParser as _ConfigParser
import traceback
from alembic.command import upgrade from alembic.command import upgrade
from alembic.config import Config from alembic.config import Config
+1 -1
View File
@@ -1,3 +1,4 @@
from app.adapters.system import rust as rust_accelerator
from app.domain.context import configure_tmdb_image_url_builder from app.domain.context import configure_tmdb_image_url_builder
from app.domain.media import configure_search_source_provider from app.domain.media import configure_search_source_provider
from app.domain.meta.customization import configure_customization_provider from app.domain.meta.customization import configure_customization_provider
@@ -5,7 +6,6 @@ from app.domain.meta.releasegroup import configure_release_groups_provider
from app.domain.meta.runtime import configure_recognition_runtime from app.domain.meta.runtime import configure_recognition_runtime
from app.domain.meta.words import configure_custom_words_provider from app.domain.meta.words import configure_custom_words_provider
from app.domain.metainfo import clear_rust_parse_options_cache from app.domain.metainfo import clear_rust_parse_options_cache
from app.adapters.system import rust as rust_accelerator
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
@@ -17,7 +17,6 @@ from app.runtime.managed_resources import (
configure_managed_resource_runtime, configure_managed_resource_runtime,
) )
_runtime_lock = threading.RLock() _runtime_lock = threading.RLock()
_managed_resource_runtime: Optional[CapabilityRuntime] = None _managed_resource_runtime: Optional[CapabilityRuntime] = None
+104 -105
View File
@@ -3,7 +3,7 @@ import inspect
import sys import sys
from typing import Callable from typing import Callable
from app.adapters.cache.redis import RedisHelper, AsyncRedisHelper from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.chain.tmdb import TmdbChain from app.chain.tmdb import TmdbChain
@@ -17,53 +17,57 @@ except ImportError as e:
sys.exit(1) sys.exit(1)
from app.adapters.system.host import SystemUtils from app.adapters.system.host import SystemUtils
from app.runtime.config import settings as legacy_settings
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
from app.runtime.config import settings as legacy_settings from app.runtime.stop import runtime_stop_state
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.cache import AsyncFileCache, FileCache from app.adapters.external.server import (
from app.runtime.extensions.module_manager import ModuleManager MoviePilotServerHelper,
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher configure_server_application_services,
from app.runtime.extensions.plugin_manager import PluginManager )
from app.runtime.events import EventHandlerBinding, EventManager
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.observability import record_metric
from app.runtime.state import SystemHelper
from app.runtime.settings import configure_runtime_setting_provider
from app.runtime.thread import ThreadHelper
from app.adapters.network.doh import DohHelper from app.adapters.network.doh import DohHelper
from app.adapters.system.resource import ( from app.adapters.system.resource import (
ResourceHelper, ResourceHelper,
configure_resource_version_provider, configure_resource_version_provider,
) )
from app.application.messaging.message import ( from app.adapters.web.security.access import set_superuser_token_payload_provider
MessageHelper, from app.api.data import ApiDataPorts, configure_api_data_runtime
MessageQueueManager, from app.application.agentdata import configure_agent_data_ports
stop_message, from app.application.agenttask import (
AgentTaskExecutionService,
configure_agent_task_execution,
)
from app.application.chain.context import (
ChainRuntimeContext,
configure_chain_runtime_context_provider,
)
from app.application.chain.data import configure_chain_data_ports, get_chain_data_ports
from app.application.chain.durable_events import (
restore_download_added,
restore_transfer_result,
) )
from app.application.configuration import ( from app.application.configuration import (
RuntimeConfiguration, RuntimeConfiguration,
RuntimeSettingsService, RuntimeSettingsService,
SystemConfigService, SystemConfigService,
get_configured_system_config,
TransferRetryConfig, TransferRetryConfig,
configure_token_runtime_config,
configure_runtime_configuration, configure_runtime_configuration,
configure_runtime_settings, configure_runtime_settings,
configure_system_config, configure_system_config,
configure_token_runtime_config,
configure_transfer_retry_config, configure_transfer_retry_config,
) get_configured_system_config,
from app.startup.composition.configuration import (
build_api_runtime_config,
build_chain_runtime_config,
build_scheduler_runtime_config,
build_token_runtime_config,
) )
from app.application.database import configure_database_governance from app.application.database import configure_database_governance
from app.application.service import configure_service_directory from app.application.history import configure_transfer_history_provider
from app.application.plugin.runtime import configure_plugin_runtime from app.application.image import configure_wallpaper_providers
from app.application.module import configure_module_runtime from app.application.messaging.agent import (
dispatch_web_agent_message_event,
shutdown_web_agent_background_tasks,
wait_web_agent_background_tasks,
)
from app.application.messaging.chat import ( from app.application.messaging.chat import (
AgentChatPersistenceService, AgentChatPersistenceService,
AgentChatService, AgentChatService,
@@ -71,43 +75,58 @@ from app.application.messaging.chat import (
configure_agent_chat_service, configure_agent_chat_service,
get_configured_agent_chat_persistence, get_configured_agent_chat_persistence,
) )
from app.application.messaging.agent import ( from app.application.messaging.message import (
dispatch_web_agent_message_event, MessageHelper,
shutdown_web_agent_background_tasks, MessageQueueManager,
wait_web_agent_background_tasks, stop_message,
) )
from app.application.security.user import configure_user_lookups from app.application.module import configure_module_runtime
from app.application.security.auth import AuthService, configure_auth_service
from app.application.security.passkeys import PasskeyService, configure_passkey_service
from app.application.security.url import close_image_proxy_block_log_coalescer
from app.application.security.userconfig import (
UserConfigurationService,
configure_user_configuration,
)
from app.application.history import configure_transfer_history_provider
from app.application.outbox import ( from app.application.outbox import (
OutboxDispatcher, OutboxDispatcher,
configure_outbox_dispatcher, configure_outbox_dispatcher,
durable_event_topic, durable_event_topic,
validate_durable_event_handlers, validate_durable_event_handlers,
) )
from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository from app.application.plugin.runtime import configure_plugin_runtime
from app.application.site.query import SiteQueryService, configure_site_query_service from app.application.security.auth import AuthService, build_superuser_token_payload, configure_auth_service
from app.application.site.health import SiteHealthService, configure_site_health_service from app.application.security.passkeys import PasskeyService, configure_passkey_service
from app.application.workflow import WorkflowQueryService, configure_workflow_query from app.application.security.url import close_image_proxy_block_log_coalescer
from app.application.agentdata import configure_agent_data_ports from app.application.security.user import configure_user_lookups
from app.application.agenttask import ( from app.application.security.userconfig import (
AgentTaskExecutionService, UserConfigurationService,
configure_agent_task_execution, configure_user_configuration,
)
from app.api.data import ApiDataPorts, configure_api_data_runtime
from app.application.subscription.write import configure_subscribe_writer
from app.adapters.external.server import (
MoviePilotServerHelper,
configure_server_application_services,
) )
from app.application.server.report import ServerReportService from app.application.server.report import ServerReportService
from app.application.server.share import ServerSharingService from app.application.server.share import ServerSharingService
from app.application.service import configure_service_directory
from app.application.site.health import SiteHealthService, configure_site_health_service
from app.application.site.query import SiteQueryService, configure_site_query_service
from app.application.subscription.write import configure_subscribe_writer
from app.application.workflow import WorkflowQueryService, configure_workflow_query
from app.command import CommandChain
from app.db.adapters.chain import TransactionalChainDurableEventWriter
from app.db.adapters.download import TransactionalDownloadFailureRepository
from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
from app.db.adapters.site import TransactionalSiteRepository
from app.db.adapters.subscription import TransactionalSubscribeWriter
from app.db.adapters.transaction import TransactionalWriteRunner
from app.db.adapters.workflow import TransactionalWorkflowExecutionService
from app.db.oper.agentchat import AgentChatOper
from app.db.oper.agenttask import AgentTaskOper
from app.db.oper.downloadhistory import DownloadHistoryOper
from app.db.oper.mediaserver import MediaServerOper
from app.db.oper.message import MessageOper
from app.db.oper.passkey import PassKeyOper
from app.db.oper.plugindata import PluginDataOper
from app.db.oper.site import SiteOper
from app.db.oper.subscribe import SubscribeOper
from app.db.oper.subscribehistory import SubscribeHistoryOper
from app.db.oper.systemconfig import SystemConfigOper
from app.db.oper.transferhistory import TransferHistoryOper
from app.db.oper.transferpending import TransferPendingOper
from app.db.oper.user import UserOper
from app.db.oper.userconfig import UserConfigOper
from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer
from app.db.session import ( from app.db.session import (
SessionFactory, SessionFactory,
async_session_scope, async_session_scope,
@@ -115,47 +134,35 @@ from app.db.session import (
get_async_db, get_async_db,
get_db, get_db,
) )
from app.db.worker import DatabaseWorker
from app.db.uow import ( from app.db.uow import (
SqlAlchemyAsyncUnitOfWork, SqlAlchemyAsyncUnitOfWork,
SqlAlchemyUnitOfWork, SqlAlchemyUnitOfWork,
configure_transaction_runners, configure_transaction_runners,
) )
from app.db.oper.subscribe import SubscribeOper from app.db.worker import DatabaseWorker
from app.db.oper.agentchat import AgentChatOper from app.runtime.cache import AsyncFileCache, FileCache
from app.db.oper.agenttask import AgentTaskOper from app.runtime.events import EventHandlerBinding, EventManager
from app.db.oper.user import UserOper from app.runtime.execution import run_in_threadpool_to_completion
from app.db.oper.passkey import PassKeyOper from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
from app.db.oper.userconfig import UserConfigOper from app.runtime.extensions.module_manager import ModuleManager
from app.db.oper.transferhistory import TransferHistoryOper from app.runtime.extensions.plugin_manager import PluginManager
from app.db.oper.downloadhistory import DownloadHistoryOper from app.runtime.extensions.service_config import (
from app.db.oper.transferpending import TransferPendingOper ServiceConfigHelper,
from app.db.oper.mediaserver import MediaServerOper configure_service_config_reader,
from app.db.oper.site import SiteOper )
from app.db.oper.message import MessageOper from app.runtime.observability import record_metric
from app.db.oper.subscribehistory import SubscribeHistoryOper from app.runtime.settings import configure_runtime_setting_provider
from app.db.oper.plugindata import PluginDataOper from app.runtime.state import SystemHelper
from app.db.oper.systemconfig import SystemConfigOper from app.runtime.tasks import get_task_registry
from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer from app.runtime.thread import ThreadHelper
from app.command import CommandChain from app.schemas.message import Message, MessageType
from app.schemas.message import Message
from app.schemas.message import MessageType
from app.schemas.types import EventType, SystemConfigKey from app.schemas.types import EventType, SystemConfigKey
from app.startup.initializers.agent import init_agent from app.startup.composition.configuration import (
from app.startup.composition.database import build_database_governance build_api_runtime_config,
from app.startup.initializers.managed_resources import ( build_chain_runtime_config,
init_managed_resources, build_scheduler_runtime_config,
stop_managed_resources, build_token_runtime_config,
) )
from app.db.adapters.subscription import TransactionalSubscribeWriter
from app.startup.composition.subscription import (
configure_transactional_subscription_scopes,
)
from app.db.adapters.chain import TransactionalChainDurableEventWriter
from app.db.adapters.download import TransactionalDownloadFailureRepository
from app.db.adapters.site import TransactionalSiteRepository
from app.db.adapters.workflow import TransactionalWorkflowExecutionService
from app.db.adapters.transaction import TransactionalWriteRunner
from app.startup.composition.context import ( from app.startup.composition.context import (
AgentChatRuntime, AgentChatRuntime,
AuthenticationRuntime, AuthenticationRuntime,
@@ -167,24 +174,15 @@ from app.startup.composition.context import (
SubscriptionRuntime, SubscriptionRuntime,
WorkflowRuntime, WorkflowRuntime,
) )
from app.adapters.web.security.access import set_superuser_token_payload_provider from app.startup.composition.database import build_database_governance
from app.application.security.auth import build_superuser_token_payload from app.startup.composition.subscription import (
from app.application.image import configure_wallpaper_providers configure_transactional_subscription_scopes,
from app.application.chain.context import (
ChainRuntimeContext,
configure_chain_runtime_context_provider,
) )
from app.application.chain.durable_events import ( from app.startup.initializers.agent import init_agent
restore_download_added, from app.startup.initializers.managed_resources import (
restore_transfer_result, init_managed_resources,
stop_managed_resources,
) )
from app.application.chain.data import configure_chain_data_ports, get_chain_data_ports
from app.runtime.extensions.service_config import (
ServiceConfigHelper,
configure_service_config_reader,
)
from app.runtime.tasks import get_task_registry
_database_worker: DatabaseWorker | None = None _database_worker: DatabaseWorker | None = None
@@ -252,6 +250,7 @@ def _build_chain_runtime_context() -> ChainRuntimeContext:
configuration=build_chain_runtime_config(settings), configuration=build_chain_runtime_config(settings),
data_ports=get_chain_data_ports(), data_ports=get_chain_data_ports(),
durable_event_writer=TransactionalChainDurableEventWriter(SessionFactory), durable_event_writer=TransactionalChainDurableEventWriter(SessionFactory),
stop_state=runtime_stop_state,
) )
@@ -503,7 +502,7 @@ def stop_frontend():
or not SystemUtils.is_windows(): or not SystemUtils.is_windows():
return return
import subprocess import subprocess
subprocess.Popen(f"taskkill /f /im nginx.exe", shell=True) subprocess.Popen("taskkill /f /im nginx.exe", shell=True)
def clear_temp(): def clear_temp():
+29 -29
View File
@@ -1,29 +1,36 @@
from pathlib import Path from pathlib import Path
from app.application.plugin.routes import register_plugin_api
from app.runtime.compat.diagnostics import ( from app.runtime.compat.diagnostics import (
configure_legacy_import_diagnostics, configure_legacy_import_diagnostics,
scan_plugin_legacy_imports, scan_plugin_legacy_imports,
) )
from app.runtime.compat.resource_imports import scan_plugin_resource_imports from app.runtime.compat.resource_imports import scan_plugin_resource_imports
from app.application.plugin.routes import register_plugin_api
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.extensions.plugin_manager import ( from app.adapters.external.market import (
PluginManager, VERSION_BACKWARD_COMPATIBLE_FLAGS,
configure_plugin_catalog_factory, PluginHelper,
configure_plugin_install_reporter, configure_installed_plugins_provider,
configure_plugin_legacy_import_services,
configure_plugin_route_refresher,
configure_plugin_resource_import_preparer,
configure_site_auth_level_provider,
) )
from app.runtime.execution import run_in_threadpool_to_completion from app.adapters.external.plugin.client import PluginMarketClient
from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.system.host import SystemUtils
from app.adapters.system.plugin.dependency import PluginDependencyInstaller
from app.adapters.system.plugin.manifest import dependency_manifest_status
from app.adapters.system.plugin.package import PluginPackageManager
from app.application.configuration import get_configured_system_config
from app.application.plugin.catalog import PluginCatalogService from app.application.plugin.catalog import PluginCatalogService
from app.application.plugin.data import DeletePluginDataCommand from app.application.plugin.data import DeletePluginDataCommand
from app.adapters.external.plugin.client import PluginMarketClient from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.db.oper.plugindata import PluginDataOper
from app.db.session import SessionFactory
from app.db.uow import SqlAlchemyUnitOfWork
from app.foundation.version import compare_version
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult
from app.runtime.extensions.plugin.storage import ( from app.runtime.extensions.plugin.storage import (
PluginStorage, PluginStorage,
configure_plugin_storage, configure_plugin_storage,
@@ -32,26 +39,19 @@ from app.runtime.extensions.plugin.system import (
PluginSystemServices, PluginSystemServices,
configure_plugin_system, configure_plugin_system,
) )
from app.runtime.managed_resources import acquire_managed_resource from app.runtime.extensions.plugin_manager import (
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module PluginManager,
from app.adapters.external.server import MoviePilotServerHelper configure_plugin_catalog_factory,
from app.adapters.external.market import ( configure_plugin_install_reporter,
PluginHelper, configure_plugin_legacy_import_services,
VERSION_BACKWARD_COMPATIBLE_FLAGS, configure_plugin_resource_import_preparer,
configure_installed_plugins_provider, configure_plugin_route_refresher,
configure_site_auth_level_provider,
) )
from app.adapters.system.plugin.dependency import PluginDependencyInstaller
from app.adapters.system.plugin.manifest import dependency_manifest_status
from app.adapters.system.plugin.package import PluginPackageManager
from app.adapters.system.host import SystemUtils
from app.db.oper.plugindata import PluginDataOper
from app.application.configuration import get_configured_system_config
from app.db.session import SessionFactory
from app.db.uow import SqlAlchemyUnitOfWork
from app.runtime.log import logger from app.runtime.log import logger
from app.foundation.version import compare_version from app.runtime.managed_resources import acquire_managed_resource
from app.schemas.plugin import PluginRuntimeStatus
from app.schemas.exception import PluginMutationRejectedError from app.schemas.exception import PluginMutationRejectedError
from app.schemas.plugin import PluginRuntimeStatus
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
+1
View File
@@ -1,5 +1,6 @@
from fastapi import FastAPI from fastapi import FastAPI
def init_routers(app: FastAPI, api_prefix: str = "/api/v1"): def init_routers(app: FastAPI, api_prefix: str = "/api/v1"):
""" """
初始化路由 初始化路由
-1
View File
@@ -1,7 +1,6 @@
from app.application.workflow import configure_workflow_runtime from app.application.workflow import configure_workflow_runtime
from app.workflow import WorkFlowManager from app.workflow import WorkFlowManager
# 启动模块是 concrete WorkFlowManager 的唯一宿主装配边界。 # 启动模块是 concrete WorkFlowManager 的唯一宿主装配边界。
configure_workflow_runtime(lambda: WorkFlowManager()) configure_workflow_runtime(lambda: WorkFlowManager())
+20 -17
View File
@@ -9,6 +9,7 @@ from typing import Awaitable, Callable
from fastapi import FastAPI from fastapi import FastAPI
from app.startup.initializers.cache import configure_cache_dependencies from app.startup.initializers.cache import configure_cache_dependencies
# 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。 # 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。
configure_cache_dependencies() configure_cache_dependencies()
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁 # urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
@@ -24,23 +25,29 @@ try:
except Exception: except Exception:
pass pass
from app.chain.system import SystemChain
from app.application.plugin.lifecycle import plugin_lifecycle from app.application.plugin.lifecycle import plugin_lifecycle
from app.application.plugin.runtime import get_plugin_manager from app.application.plugin.runtime import get_plugin_manager
from app.chain.system import SystemChain
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled from app.runtime.stop import runtime_stop_state
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.runtime.health import get_application_health
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.topology import validate_process_topology
from app.runtime.tasks import TaskRegistry, configure_task_registry
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.network.http import (
aclose_shared_async_transports,
configure_default_user_agent,
)
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.health import get_application_health
from app.runtime.log import LoggerManager, logger
from app.runtime.state import SystemHelper from app.runtime.state import SystemHelper
from app.runtime.log import logger, LoggerManager from app.runtime.tasks import TaskRegistry, configure_task_registry
from app.startup.initializers.command import init_command, restart_command from app.runtime.topology import validate_process_topology
from app.startup.initializers.agent import stop_agent from app.startup.initializers.agent import stop_agent
from app.startup.initializers.command import init_command, restart_command
from app.startup.initializers.domain import configure_domain_dependencies from app.startup.initializers.domain import configure_domain_dependencies
from app.startup.initializers.modules import ( from app.startup.initializers.modules import (
drain_events, drain_events,
@@ -48,7 +55,7 @@ from app.startup.initializers.modules import (
settle_events, settle_events,
stop_modules, stop_modules,
) )
from app.startup.initializers.monitor import stop_monitor, init_monitor from app.startup.initializers.monitor import init_monitor, stop_monitor
from app.startup.initializers.plugins import ( from app.startup.initializers.plugins import (
configure_plugin_services, configure_plugin_services,
execute_task, execute_task,
@@ -61,11 +68,10 @@ from app.startup.initializers.plugins import (
) )
from app.startup.initializers.routers import init_routers from app.startup.initializers.routers import init_routers
from app.startup.initializers.scheduler import ( from app.startup.initializers.scheduler import (
stop_scheduler,
init_scheduler,
init_plugin_scheduler, init_plugin_scheduler,
init_scheduler,
stop_scheduler,
) )
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
from app.startup.initializers.transfer import ( from app.startup.initializers.transfer import (
replay_pending_transfers, replay_pending_transfers,
stop_transfer_runtime, stop_transfer_runtime,
@@ -77,10 +83,6 @@ from app.startup.lifecycle.components import (
LifecycleMode, LifecycleMode,
lifecycle_manifest, lifecycle_manifest,
) )
from app.adapters.network.http import (
aclose_shared_async_transports,
configure_default_user_agent,
)
async def init_extra(): async def init_extra():
@@ -541,7 +543,8 @@ def build_lifecycle_components(app: FastAPI) -> tuple[LifecycleComponent, ...]:
# 停止信号必须先于一切资源释放发出,让工作流、整理等长任务尽早感知停机。 # 停止信号必须先于一切资源释放发出,让工作流、整理等长任务尽早感知停机。
LifecycleComponent( LifecycleComponent(
name="停止信号", name="停止信号",
stop=global_vars.stop_system, # 兼容旧测试与插件;GlobalVar 内部已委托到 StopState。
stop=getattr(global_vars, "stop_system"),
stop_order=4, stop_order=4,
stop_timeout_seconds=10, stop_timeout_seconds=10,
), ),
+7 -9
View File
@@ -4,18 +4,16 @@ from typing import Any, Dict, List, Optional, Tuple
from pydantic import BaseModel from pydantic import BaseModel
from app.runtime.config import global_vars
from app.runtime.events import eventmanager, Event
from app.application.chain.data import get_chain_workflow_port from app.application.chain.data import get_chain_workflow_port
from app.application.workflow import WorkflowExecutionOwner from app.application.workflow import WorkflowExecutionOwner
from app.foundation.reflection import ModuleHelper from app.foundation.reflection import ModuleHelper
from app.runtime.log import logger
from app.schemas.workflow import ActionContext
from app.schemas.workflow import Action
from app.schemas.workflow import ActionResult
from app.schemas.workflow import Workflow
from app.schemas.types import EventType
from app.foundation.singleton import Singleton from app.foundation.singleton import Singleton
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.types import EventType
from app.schemas.workflow import Action, ActionContext, ActionResult, Workflow
_WORKFLOW_STOP_TIMEOUT_SECONDS = 10.0 _WORKFLOW_STOP_TIMEOUT_SECONDS = 10.0
@@ -283,7 +281,7 @@ class WorkFlowManager(metaclass=Singleton):
def _is_cancelled(workflow_id: int, cancel_token: Optional[Any]) -> bool: def _is_cancelled(workflow_id: int, cancel_token: Optional[Any]) -> bool:
if cancel_token and cancel_token.is_cancelled(): if cancel_token and cancel_token.is_cancelled():
return True return True
return global_vars.is_workflow_stopped(workflow_id) return runtime_stop_state.is_workflow_stopped(workflow_id)
def _sleep_with_cancel(self, workflow_id: int, seconds: float, cancel_token: Optional[Any]) -> None: def _sleep_with_cancel(self, workflow_id: int, seconds: float, cancel_token: Optional[Any]) -> None:
deadline = monotonic() + seconds deadline = monotonic() + seconds
+2 -4
View File
@@ -1,11 +1,9 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, ClassVar, Union from typing import Any, ClassVar, Union
from app.chain import ChainBase
from app.application.configuration import get_configured_system_config from app.application.configuration import get_configured_system_config
from app.schemas.workflow import ActionContext from app.chain import ChainBase
from app.schemas.workflow import ActionParams from app.schemas.workflow import ActionContext, ActionParams, ActionResult
from app.schemas.workflow import ActionResult
class ActionChain(ChainBase): class ActionChain(ChainBase):
+4 -6
View File
@@ -2,16 +2,14 @@ from typing import Optional
from pydantic import Field from pydantic import Field
from app.workflow.actions import BaseAction
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.media import MediaChain from app.chain.media import MediaChain
from app.runtime.config import global_vars
from app.domain.metainfo import MetaInfo from app.domain.metainfo import MetaInfo
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.workflow import ActionParams from app.runtime.stop import runtime_stop_state
from app.schemas.workflow import ActionContext
from app.schemas.workflow import DownloadTask
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.schemas.workflow import ActionContext, ActionParams, DownloadTask
from app.workflow.actions import BaseAction
class AddDownloadParams(ActionParams): class AddDownloadParams(ActionParams):
@@ -55,7 +53,7 @@ class AddDownloadAction(BaseAction):
params = AddDownloadParams(**params) params = AddDownloadParams(**params)
_started = False _started = False
for t in context.torrents: for t in context.torrents:
if global_vars.is_workflow_stopped(workflow_id): if runtime_stop_state.is_workflow_stopped(workflow_id):
break break
# 检查缓存 # 检查缓存
cache_key = f"{t.torrent_info.site}-{t.torrent_info.title}" cache_key = f"{t.torrent_info.site}-{t.torrent_info.title}"
+7 -8
View File
@@ -1,12 +1,11 @@
from app.workflow.actions import BaseAction
from app.chain.subscribe import SubscribeChain
from app.application.configuration import get_chain_runtime_config_snapshot
from app.runtime.config import global_vars
from app.domain.context import MediaInfo
from app.application.chain.data import get_chain_subscribe_port from app.application.chain.data import get_chain_subscribe_port
from app.application.configuration import get_chain_runtime_config_snapshot
from app.chain.subscribe import SubscribeChain
from app.domain.context import MediaInfo
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.workflow import ActionParams from app.runtime.stop import runtime_stop_state
from app.schemas.workflow import ActionContext from app.schemas.workflow import ActionContext, ActionParams
from app.workflow.actions import BaseAction
class AddSubscribeParams(ActionParams): class AddSubscribeParams(ActionParams):
@@ -45,7 +44,7 @@ class AddSubscribeAction(BaseAction):
""" """
_started = False _started = False
for media in context.medias: for media in context.medias:
if global_vars.is_workflow_stopped(workflow_id): if runtime_stop_state.is_workflow_stopped(workflow_id):
break break
# 检查缓存 # 检查缓存
cache_key = f"{media.type}-{media.title}-{media.year}-{media.season}" cache_key = f"{media.type}-{media.title}-{media.year}-{media.season}"
+4 -5
View File
@@ -1,8 +1,7 @@
from app.workflow.actions import BaseAction, ActionChain
from app.runtime.config import global_vars
from app.schemas.workflow import ActionParams
from app.schemas.workflow import ActionContext
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.workflow import ActionContext, ActionParams
from app.workflow.actions import ActionChain, BaseAction
class FetchDownloadsParams(ActionParams): class FetchDownloadsParams(ActionParams):
@@ -45,7 +44,7 @@ class FetchDownloadsAction(BaseAction):
return context return context
for download in self._downloads: for download in self._downloads:
if global_vars.is_workflow_stopped(workflow_id): if runtime_stop_state.is_workflow_stopped(workflow_id):
break break
logger.info(f"获取下载任务 {download.download_id} 状态 ...") logger.info(f"获取下载任务 {download.download_id} 状态 ...")
torrents = ActionChain().list_torrents( torrents = ActionChain().list_torrents(
+6 -8
View File
@@ -2,18 +2,16 @@ from typing import List, Optional
from pydantic import Field from pydantic import Field
from app.workflow.actions import BaseAction from app.adapters.network.http import RequestUtils
from app.chain.recommend import RecommendChain
from app.application.configuration import get_chain_runtime_config_snapshot from app.application.configuration import get_chain_runtime_config_snapshot
from app.schemas.workflow import ActionParams from app.chain.recommend import RecommendChain
from app.schemas.workflow import ActionContext
from app.runtime.config import global_vars
from app.runtime.events import eventmanager from app.runtime.events import eventmanager
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.stop import runtime_stop_state
from app.schemas.event import RecommendSourceEventData from app.schemas.event import RecommendSourceEventData
from app.schemas.workflow import MediaInfo
from app.schemas.types import ChainEventType from app.schemas.types import ChainEventType
from app.adapters.network.http import RequestUtils from app.schemas.workflow import ActionContext, ActionParams, MediaInfo
from app.workflow.actions import BaseAction
class FetchMediasParams(ActionParams): class FetchMediasParams(ActionParams):
@@ -141,7 +139,7 @@ class FetchMediasAction(BaseAction):
try: try:
if params.source_type == "ranking": if params.source_type == "ranking":
for api_path in params.sources: for api_path in params.sources:
if global_vars.is_workflow_stopped(workflow_id): if runtime_stop_state.is_workflow_stopped(workflow_id):
break break
source = self.__get_source(api_path) source = self.__get_source(api_path)
if not source: if not source:

Some files were not shown because too many files have changed in this diff Show More