mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-21 16:23:34 +08:00
refactor backend module architecture
This commit is contained in:
@@ -26,11 +26,20 @@ MoviePilot is a self-hosted media automation platform targeting Chinese-language
|
||||
| `app/api/endpoints/` | HTTP endpoint handlers |
|
||||
| `app/chain/` | Business orchestration layer |
|
||||
| `app/modules/` | Pluggable backend integrations (downloaders, media servers, etc.) |
|
||||
| `app/helper/` | Reusable low-level utilities |
|
||||
| `app/db/` | SQLAlchemy models and data access wrappers |
|
||||
| `app/core/` | Config, event system, module manager, plugin manager, security |
|
||||
| `app/foundation/` | Stateless general-purpose primitives |
|
||||
| `app/domain/` | Media-domain models, parsing, and rules |
|
||||
| `app/platform/` | Config, events, caching, and process-wide coordination |
|
||||
| `app/infrastructure/` | Network, filesystem, process, Redis, and site-resource adapters |
|
||||
| `app/extensions/` | Module, plugin, market, and service lifecycle management |
|
||||
| `app/integrations/` | Downloader, media-server, RSS, OCR, storage, and remote-service adapters |
|
||||
| `app/messaging/` | Messaging, interaction, notification, and push capabilities |
|
||||
| `app/security/` | Authentication and access-control capabilities |
|
||||
| `app/services/` | Focused application services |
|
||||
| `app/sdk/` | Stable imports for plugins |
|
||||
| `app/compat/` | Virtual legacy import compatibility and DEBUG diagnostics |
|
||||
| `app/schemas/` | Pydantic request/response models and shared enums |
|
||||
| `app/agent/` | LLM agent runtime |
|
||||
| `app/agent/` | LLM Agent runtime, tools, middleware, and Skill lifecycle |
|
||||
| `app/workflow/` | Workflow engine |
|
||||
| `database/versions/` | Alembic migration scripts |
|
||||
| `docs/` | CLI, MCP/API, and development workflow documentation |
|
||||
@@ -81,4 +90,4 @@ An alternative for users running from source. The `moviepilot` CLI handles insta
|
||||
| Skill | A packaged AI agent capability that can be invoked via the MCP interface |
|
||||
| SystemConfig | Runtime key-value configuration stored in the database and managed via `SystemConfigKey` |
|
||||
|
||||
*Last Updated: 2026-05-25*
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
| Web framework | FastAPI |
|
||||
| ASGI server | Uvicorn |
|
||||
| Data validation | Pydantic v2 (`BaseModel`, `BaseSettings`, `model_validator`) |
|
||||
| Settings management | `pydantic-settings` (`BaseSettings` class in `app/core/config.py`) |
|
||||
| Settings management | `pydantic-settings` (`BaseSettings` class in `app/platform/config.py`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
| Item | Detail |
|
||||
|---|---|
|
||||
| File-based cache | `FileCache` / `AsyncFileCache` in `app/core/cache.py` |
|
||||
| File-based cache | `FileCache` / `AsyncFileCache` in `app/platform/cache.py` |
|
||||
| Redis | Optional; `app/modules/redis/` module; used for distributed caching when configured |
|
||||
| In-process cache | Decorator helpers `fresh` / `async_fresh` on `FileCache` |
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ bash scripts/collect-site-adapter.sh
|
||||
# Run after activating the project virtual environment
|
||||
python -m scripts.generate_plugin_market_default \
|
||||
--wiki-file /path/to/MoviePilot-Wiki/plugin.md \
|
||||
--config-file app/core/config.py
|
||||
--config-file app/platform/config.py
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
@@ -84,12 +84,12 @@ result = await self.async_run_module("method_name", kwarg1=val1)
|
||||
|
||||
**When to use:** Triggering cross-cutting reactions (e.g., notifying the media server after a transfer completes, reloading a module after config changes, dispatching user messages to message channels).
|
||||
|
||||
**Core classes:** `EventManager` (singleton instance `eventmanager`) and `Event` in `app/core/event.py`.
|
||||
**Core classes:** `EventManager` (singleton instance `eventmanager`) and `Event` in `app/platform/events.py`.
|
||||
|
||||
**Registering a handler:**
|
||||
|
||||
```python
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.platform.events import eventmanager, Event
|
||||
from app.schemas.types import EventType
|
||||
|
||||
@eventmanager.register(EventType.TransferComplete)
|
||||
@@ -136,7 +136,7 @@ oper.add(Subscribe(name="Example", type="电影"))
|
||||
|
||||
**When to use:** A chain, module, or helper holds a long-lived object that must be rebuilt when specific configuration keys change (e.g., a downloader client reconnects when its host/port changes).
|
||||
|
||||
**Mixin:** `ConfigReloadMixin` in `app/utils/mixins.py`
|
||||
**Mixin:** `ConfigReloadMixin` in `app/platform/reload.py`
|
||||
|
||||
**How it works:**
|
||||
1. Inherit `ConfigReloadMixin`.
|
||||
@@ -161,10 +161,10 @@ class MyChain(ChainBase, ConfigReloadMixin):
|
||||
|
||||
**When to use:** Classes that must have exactly one instance shared application-wide (e.g., `EventManager`, `ModuleManager`, `PluginManager`).
|
||||
|
||||
**Implementation:** Inherit from `Singleton` in `app/utils/singleton.py`.
|
||||
**Implementation:** Inherit from `Singleton` in `app/foundation/singleton.py`.
|
||||
|
||||
```python
|
||||
from app.utils.singleton import Singleton
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
class MyManager(metaclass=Singleton):
|
||||
...
|
||||
@@ -209,11 +209,11 @@ Usage mirrors `SystemConfigOper` but scoped to a `user_id`.
|
||||
|
||||
| Anti-Pattern | Correct Alternative |
|
||||
|---|---|
|
||||
| `module -> chain` coupling | Move shared logic into `chain` or down into `helper` |
|
||||
| `module -> chain` coupling | Move orchestration into `chain` and shared logic into its owning canonical package |
|
||||
| `module -> module` direct calls | Use `chain` to orchestrate cross-module workflows |
|
||||
| `helper -> chain` dependency | `helper` must remain a low-level utility; move orchestration to `chain` |
|
||||
| Lower-level module importing a chain or manager | Register a callback/resolver from `app/startup/` or move orchestration to `chain` |
|
||||
| Raw SQLAlchemy queries in endpoints or chains | Use the corresponding `*_oper.py` class |
|
||||
| Raw string keys for SystemConfig | Define and use a `SystemConfigKey` enum entry |
|
||||
| HTTP requests via `requests` or `httpx` directly | Use `RequestUtils` from `app/utils/http.py` |
|
||||
| HTTP requests via `requests` or `httpx` directly | Host code uses `RequestUtils` from `app/foundation/http.py`; plugins use `app.sdk.network` |
|
||||
|
||||
*Last Updated: 2026-05-25*
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -1,173 +1,250 @@
|
||||
# 05 — Architecture and Modules
|
||||
# 05 - Architecture and Modules
|
||||
|
||||
## Layer Overview
|
||||
## Dependency Model
|
||||
|
||||
The application is structured as four distinct layers. Each layer has a defined responsibility, and dependency may only flow in permitted directions.
|
||||
MoviePilot uses explicit capability packages instead of the historical
|
||||
`app/core`, `app/helper`, and `app/utils` buckets. Physical Python source must
|
||||
not be added back under those paths. They exist only as virtual compatibility
|
||||
packages for installed plugins.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Entrypoints │
|
||||
│ (API Endpoints / CLI / Agent / Scheduler / │
|
||||
│ Webhook / Message Interaction) │
|
||||
└────────────────────┬─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Chain Layer (app/chain/) │
|
||||
│ Business orchestration: search, download, │
|
||||
│ subscribe, transfer, message, recommend, etc. │
|
||||
└──────┬──────────────┬───────────────┬────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌──────────┐ ┌────────────────┐
|
||||
│ Module │ │ Helper │ │ DB / Oper │
|
||||
│ Layer │ │ Layer │ │ Layer │
|
||||
│ (app/ │ │ (app/ │ │ (app/db/) │
|
||||
│ modules/) │ │ helper/)│ │ │
|
||||
└────────────┘ └──────────┘ └────────────────┘
|
||||
Every migrated capability module and boundary package is required to stay out
|
||||
of Python-module import cycles. The gate builds the complete application graph
|
||||
so a cycle through an unmigrated caller is still detected. Startup code is the
|
||||
composition root: it wires callbacks, resolvers, and adapters into lower-level
|
||||
managers instead of letting those managers import and instantiate higher-level
|
||||
services.
|
||||
|
||||
```text
|
||||
Entrypoints / Plugins
|
||||
|
|
||||
v
|
||||
API / Agent / CLI / Scheduler / Workflow
|
||||
|
|
||||
v
|
||||
Chain orchestration -----> Modules / DB / Services
|
||||
| |
|
||||
+-------------------------+
|
||||
|
|
||||
v
|
||||
Domain / Platform contracts and state
|
||||
|
|
||||
v
|
||||
Foundation and infrastructure adapters
|
||||
|
||||
Startup composes managers, adapters, diagnostics, and error callbacks.
|
||||
Compatibility aliases and the plugin SDK are boundary packages, never
|
||||
dependencies of canonical implementation modules.
|
||||
```
|
||||
|
||||
---
|
||||
## Canonical Capability Packages
|
||||
|
||||
## Layer Responsibilities and Boundaries
|
||||
| Package | Ownership |
|
||||
|---|---|
|
||||
| `app/foundation/` | Reusable low-level mechanisms with no MoviePilot business/config dependency: HTTP clients, dynamic module loading, crypto, DOM, identity, URL, version, singleton, text segmentation, and data structures |
|
||||
| `app/domain/` | Pure business semantics for media, recognition, sites, and torrents; configuration, persistence, and acceleration are injected; detailed below |
|
||||
| `app/platform/` | Process-wide config, events, complete logging runtime, cache contracts/in-memory policy, execution policy, localization, scheduling, runtime lifecycle, concurrency, GC monitoring, and rate limits |
|
||||
| `app/infrastructure/` | Configured runtime adapters for Redis/file cache, standard streams, browser, DNS/network, RSS, resources, packages, OS, Rust acceleration, and generated site resources |
|
||||
| `app/extensions/` | Runtime module, plugin, and service discovery/lifecycle management |
|
||||
| `app/integrations/` | Concrete product/ecosystem integrations: plugin markets and repositories, CookieCloud, IP-location providers, OCR, and remote MoviePilot service |
|
||||
| `app/messaging/` | Agent-message bridge, message rendering/routing, and interactions |
|
||||
| `app/security/` | Authentication, authorization, URL/path safety, SSRF protection, OTP, cookies, passkeys, and two-factor authentication |
|
||||
| `app/services/` | Focused application services: audio, directory, downloader/media-server/storage selection, notification selection, media-server normalization/matching, persisted recognition/filter rules, formatting, image, torrent I/O, and transfer history |
|
||||
| `app/agent/skills/` | Agent Skill metadata, market discovery, installation, and local lifecycle; importing it must not initialize the Agent orchestrator |
|
||||
| `app/sdk/` | Stable, deliberately curated imports for plugin authors |
|
||||
| `app/compat/` | Standard-library-only legacy import routing and DEBUG diagnostics |
|
||||
|
||||
### Shared File Placement Rule
|
||||
`app/chain/` remains the application orchestration layer and `app/modules/`
|
||||
remains the collection of pluggable backend implementations. A package name
|
||||
describes ownership; it does not authorize a dependency cycle. The architecture
|
||||
gate checks the complete module graph, including imports outside these packages.
|
||||
|
||||
Before creating a new file under `app/api/endpoints/`, `app/chain/`, `app/helper/`, or `app/utils/`, first check whether the capability belongs in an existing domain file. Prefer extending that file when the domain already exists. Create a new file only for a genuinely new domain or standalone reusable concern, and name it with a single noun according to `07-naming-conventions.md`.
|
||||
### Domain subdomains
|
||||
|
||||
### Entrypoint Layer
|
||||
`app/domain/` is a business-capability package, not a synonym for every file
|
||||
whose name mentions media, site, or torrent:
|
||||
|
||||
**Directories:** `app/api/endpoints/`, `moviepilot` (CLI), `app/agent/`, scheduler callbacks, webhook handlers, message interactions.
|
||||
| Subdomain | Modules and ownership |
|
||||
|---|---|
|
||||
| Media | `context.py` owns `MediaInfo`, `TorrentInfo`, music models, and use-case context; `media.py` owns source/ID normalization; `scraper.py` owns Kodi-style NFO reading and media metadata document generation |
|
||||
| Recognition | `metainfo.py`, `meta/`, and `tokens.py` parse names, paths, release groups, streaming platforms, anime, video, and music metadata |
|
||||
| Site | `site.py` interprets site HTML into business states such as logged-in and checked-in; generic HTTP transport remains foundation and configured browser access remains infrastructure |
|
||||
| Torrent | Torrent identity and title semantics live in the context/recognition model; downloading, caching, and parsing torrent files lives in `services/torrent.py` |
|
||||
| Shared business text | `string.py` retains MoviePilot-specific media/site/torrent text normalization pending concern-level extraction; new generic primitives must not be added to it |
|
||||
|
||||
**Responsibilities:**
|
||||
- HTTP concerns: authentication, parameter parsing, response model serialization, streaming adaptation, simple input validation.
|
||||
- Simple list, detail, toggle, settings read/write, and pure CRUD endpoints may call `app/db/` or a helper directly.
|
||||
- Any logic that coordinates multiple modules, triggers events, touches caches, or combines workflows must be moved into `chain`.
|
||||
Filter-rule meaning is part of the torrent/filter domain, but
|
||||
`services/filter.py` reads user-persisted rule configuration and is
|
||||
therefore an application service rather than a pure domain module.
|
||||
|
||||
**Rules:**
|
||||
- Prefer adding new endpoints to an existing domain file. Create a new endpoint file only when introducing a new top-level resource domain, and use a single-noun filename.
|
||||
- After adding a new endpoint, register it in `app/api/apiv1.py`.
|
||||
- Endpoints must not contain business logic that belongs in `chain`.
|
||||
Recognition follows the same boundary: `services/recognition.py` reads
|
||||
`SystemConfigOper`; `startup/domain_initializer.py` injects live rule providers,
|
||||
file-extension policy, TMDB image URL construction, source defaults, and the
|
||||
optional Rust accelerator into pure domain modules.
|
||||
|
||||
---
|
||||
## Shared File Placement Rule
|
||||
|
||||
### Chain Layer
|
||||
Before creating a file, first decide which capability package owns it and check
|
||||
whether an existing domain file already provides that capability. Create a new
|
||||
file only for a genuinely separate concern and name it according to
|
||||
`07-naming-conventions.md`.
|
||||
|
||||
Do not create generic `common`, `helper`, or `utils` buckets. A reusable function
|
||||
still needs an owner:
|
||||
|
||||
- Generic code that does not read MoviePilot business/config state belongs in `app/foundation/`, including reusable protocol clients and reflection helpers.
|
||||
- Core media-specific rules belong in `app/domain/`; rules tied specifically to
|
||||
configured media-server representations belong in `app/services/mediaserver.py`.
|
||||
- Configuration-aware runtime resources belong in `app/infrastructure/`; a
|
||||
concrete external product or ecosystem belongs in `app/integrations/`.
|
||||
- Stateful cross-domain behavior belongs in `app/services/` or `app/chain/`.
|
||||
- Plugin-facing public imports belong in `app/sdk/`; canonical packages are not
|
||||
automatically public plugin APIs.
|
||||
|
||||
## Entrypoint Layer
|
||||
|
||||
**Directories:** `app/api/endpoints/`, `moviepilot` (CLI), `app/agent/`, scheduler
|
||||
callbacks, webhook handlers, and message interactions.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Handle authentication, parameter parsing, response serialization, streaming,
|
||||
and boundary validation.
|
||||
- Call `app/chain/` for logic that coordinates modules, events, caches, or
|
||||
workflows.
|
||||
- Call an Oper class or focused service directly only for simple CRUD and input
|
||||
normalization.
|
||||
|
||||
Endpoints must not contain reusable business workflows. Register new API
|
||||
endpoints in `app/api/apiv1.py`.
|
||||
|
||||
## Chain Layer
|
||||
|
||||
**Directory:** `app/chain/`
|
||||
|
||||
**Responsibilities:**
|
||||
- Business orchestration shared by API, CLI, agent, scheduler, and other entrypoints.
|
||||
- Composes module capabilities, helpers, database access, events, and caches.
|
||||
- Focuses on use cases and workflows.
|
||||
Chains implement use cases shared by API, CLI, agent, scheduler, and other
|
||||
entrypoints. They may coordinate modules, services, Oper classes, events, and
|
||||
caches.
|
||||
|
||||
**Rules:**
|
||||
- Call module capabilities via `run_module()` or `async_run_module()`. Use `ModuleManager` directly only when enumerating, inspecting, or running health checks.
|
||||
- Do not hold low-level protocol details, HTTP request objects, or page-specific parameter assembly.
|
||||
- Before creating a new chain file, verify the workflow is genuinely reused across multiple entrypoints, or coordinates multiple modules. If it is short logic for a single endpoint, keep it in the endpoint.
|
||||
- Chain-to-chain calls are allowed when reusing stable domain logic. Avoid introducing new circular dependencies.
|
||||
- Call module capabilities through `run_module()` or `async_run_module()`.
|
||||
- Use `ModuleManager` directly only for enumeration, inspection, or health
|
||||
checks.
|
||||
- Chain-to-chain reuse is allowed only while the static dependency graph remains
|
||||
acyclic.
|
||||
- Do not place HTTP request objects or backend-specific protocol details here.
|
||||
|
||||
---
|
||||
|
||||
### Module Layer
|
||||
## Module Layer
|
||||
|
||||
**Directory:** `app/modules/`
|
||||
|
||||
**Responsibilities:**
|
||||
- Pluggable capability implementations: downloaders, media servers, message channels, metadata sources, storage backends, subtitle backends, filter backends, etc.
|
||||
- Manages lifecycle (init, stop), configuration switches, priority ordering, and independent testability.
|
||||
Modules implement pluggable backends such as downloaders, media servers,
|
||||
metadata sources, message channels, indexers, and storage providers.
|
||||
|
||||
**Module categories (defined in `app/schemas/types.py`):**
|
||||
- A module focuses on one backend or capability and returns domain results, not
|
||||
HTTP responses.
|
||||
- New direct `module -> module` or `module -> chain` dependencies are forbidden.
|
||||
- Cross-module orchestration belongs in a chain.
|
||||
- Shared backend-neutral behavior belongs in its owning canonical package.
|
||||
|
||||
| Enum | Examples |
|
||||
|---|---|
|
||||
| `ModuleType.Downloader` | qBittorrent, Transmission, rTorrent |
|
||||
| `ModuleType.MediaServer` | Emby, Jellyfin, Plex, TrimMedia, Zspace, Ugreen |
|
||||
| `ModuleType.MessageChannel` | Telegram, WeChat, Feishu, Slack, Discord |
|
||||
| `ModuleType.MetaData` | TMDB, TheTVDB, Douban, Bangumi, Fanart |
|
||||
| `ModuleType.Indexer` | Site-specific torrent indexers |
|
||||
| `ModuleType.Storage` | Alist, rclone, u115, local storage |
|
||||
Module categories are defined in `app/schemas/types.py`.
|
||||
|
||||
**Rules:**
|
||||
- A module must focus on one backend or one capability. It returns domain result objects, not HTTP responses, and must not depend on FastAPI request objects or endpoint auth.
|
||||
- Do not add direct `module → module` coupling for new code. Cross-module orchestration must go through `chain`.
|
||||
- Do not expand the historical `module → chain` usage pattern. If a module needs shared business logic, move that logic into `chain` or down into `helper`.
|
||||
|
||||
---
|
||||
|
||||
### Helper Layer
|
||||
|
||||
**Directory:** `app/helper/`
|
||||
|
||||
**Responsibilities:**
|
||||
- Reusable low-level support: path handling, config aggregation, site index loading, protocol wrappers, rate limiting, cache utilities, page parsing, notification helpers.
|
||||
|
||||
**Rules:**
|
||||
- Add a new helper only when the logic is reused in multiple places, or it is clearly a standalone low-level concern.
|
||||
- If logic is used only by a single chain or module, keep it in the original file. Do not turn `helper` into a dumping ground.
|
||||
- If the code needs configuration switches, runtime loading, priorities, or multi-implementation dispatch, it is a `module`, not a `helper`.
|
||||
- `helper` must not contain full business workflows.
|
||||
|
||||
---
|
||||
|
||||
### DB / Oper Layer
|
||||
## DB / Oper Layer
|
||||
|
||||
**Directory:** `app/db/`
|
||||
|
||||
**Responsibilities:**
|
||||
- SQLAlchemy models under `app/db/models/`.
|
||||
- Data access wrappers (`*_oper.py`) that encapsulate all database queries.
|
||||
SQLAlchemy models live under `app/db/models/`; `*_oper.py` classes encapsulate
|
||||
queries. Chains, modules, services, and endpoints must use those classes instead
|
||||
of issuing SQLAlchemy queries directly. Every schema change requires an Alembic
|
||||
migration under `database/versions/`.
|
||||
|
||||
**Rules:**
|
||||
- Never issue SQLAlchemy queries directly from chain, module, or endpoint code. Always use the corresponding `*_oper.py` class.
|
||||
- Any schema change requires a new Alembic migration under `database/versions/`.
|
||||
## Composition and Compatibility Boundaries
|
||||
|
||||
---
|
||||
- `app/startup/` owns process composition. Lower layers expose explicit
|
||||
registration or configuration functions for dependencies such as event
|
||||
resolvers and error reporters.
|
||||
- Cache contracts, memory implementations, decorators, and proxies live in
|
||||
`app/platform/cache.py`; Redis and file I/O implementations live in
|
||||
`app/infrastructure/cache.py`. Startup registers concrete factories before
|
||||
importing modules that instantiate cache decorators.
|
||||
- The complete logging runtime lives in `app/platform/log.py`: policy,
|
||||
console/plugin routing, async rotating file output, and shutdown.
|
||||
`app.platform.config` supplies the resolved settings and log path.
|
||||
`platform/log.py` is enforced as a dependency leaf with no `app.*` imports.
|
||||
Foundation modules do not emit runtime logs; their callers decide whether a
|
||||
returned fallback or raised error should be logged. Plugins use `app.sdk.logging`;
|
||||
legacy `app.log` resolves to that SDK facade.
|
||||
- Resource adapters only report whether installation succeeded. Process restart
|
||||
policy belongs to `app/startup/modules_initializer.py`.
|
||||
- Configured notification-service discovery lives in
|
||||
`app/services/notification.py`. Web Push subscription and manual-send HTTP
|
||||
behavior lives directly in `app/api/endpoints/message.py`, not in messaging.
|
||||
- `app/compat/` may not import canonical MoviePilot implementation modules at
|
||||
import time. Its manifest stores strings and resolves aliases lazily.
|
||||
- Canonical packages may not import `app.compat` or `app.sdk`.
|
||||
- Host code uses canonical paths. Only `app/plugins/` and compatibility tests
|
||||
may use legacy `app.core`, `app.helper`, or `app.utils` paths.
|
||||
- New plugins use `app.sdk`. In DEBUG mode, legacy plugin imports work but emit
|
||||
one actionable warning per plugin and legacy module.
|
||||
- Delayed imports are not accepted as a way to hide a dependency cycle.
|
||||
|
||||
## Permitted Call Directions
|
||||
|
||||
| Direction | Status |
|
||||
|---|---|
|
||||
| `endpoint / CLI / agent / scheduler → chain` | ✅ Preferred |
|
||||
| `endpoint / CLI / agent / scheduler → db / helper` | ✅ Allowed for simple CRUD and input normalization only |
|
||||
| `chain → chain` | ✅ Allowed when reusing stable, non-circular domain logic |
|
||||
| `chain → module` | ✅ Via `run_module()` / `async_run_module()` |
|
||||
| `chain → helper` | ✅ Allowed |
|
||||
| `chain → db` | ✅ Via `*_oper.py` classes |
|
||||
| `module → chain` | ⚠️ Exists in legacy code; do not expand in new code |
|
||||
| `module → module` | ❌ Forbidden in new code |
|
||||
| `helper → chain` | ❌ Forbidden |
|
||||
| `helper → endpoint` | ❌ Forbidden |
|
||||
|
||||
---
|
||||
| `entrypoint -> chain / service / Oper` | Allowed according to workflow complexity |
|
||||
| `chain -> module / service / Oper / canonical capability` | Allowed |
|
||||
| `module -> canonical capability / Oper` | Allowed |
|
||||
| `module -> module / chain` | Forbidden for new code |
|
||||
| `canonical implementation -> sdk / compat` | Forbidden |
|
||||
| `compat -> canonical implementation at module import time` | Forbidden; aliases resolve lazily |
|
||||
| `foundation -> other app capability packages` | Forbidden |
|
||||
| Any import that creates a module-level cycle | Forbidden |
|
||||
|
||||
## Key File Locations
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `app/api/apiv1.py` | API router registration — register new endpoints here |
|
||||
| `app/core/config.py` | `ConfigModel` and `Settings` — all deployment/env-level config |
|
||||
| `app/schemas/types.py` | `SystemConfigKey`, `EventType`, `ModuleType`, and all shared enums |
|
||||
| `app/core/module.py` | `ModuleManager` — discovers and manages module instances |
|
||||
| `app/core/plugin.py` | `PluginManager` — discovers and manages plugin instances |
|
||||
| `app/core/event.py` | `EventManager` + `Event` — the application event bus |
|
||||
| `app/core/context.py` | `Context`, `MediaInfo`, `TorrentInfo` — shared domain context objects |
|
||||
| `app/main.py` | Application startup and FastAPI instance |
|
||||
| `database/versions/` | Alembic migration scripts |
|
||||
|
||||
---
|
||||
| `app/api/apiv1.py` | API router registration |
|
||||
| `app/platform/config.py` | `ConfigModel`, `Settings`, and deployment configuration |
|
||||
| `app/platform/events.py` | `EventManager`, `Event`, and event resolver registration |
|
||||
| `app/extensions/module_manager.py` | Module discovery and lifecycle |
|
||||
| `app/extensions/plugin_manager.py` | Plugin discovery and lifecycle |
|
||||
| `app/foundation/module.py` | Generic Python module discovery and dynamic import |
|
||||
| `app/foundation/http.py` | Shared synchronous and asynchronous HTTP clients |
|
||||
| `app/infrastructure/rss.py` | Configured RSS retrieval and parsing adapter |
|
||||
| `app/platform/cache.py` | Cache contracts, memory backend, decorators, and proxies |
|
||||
| `app/infrastructure/cache.py` | Redis and filesystem cache adapters |
|
||||
| `app/platform/gc.py` | Process memory observation and garbage-collection policy |
|
||||
| `app/integrations/market.py` | Plugin repository discovery, compatibility, download, and installation |
|
||||
| `app/integrations/location.py` | External IP-location provider integration |
|
||||
| `app/agent/skills/registry.py` | Agent Skill discovery, market, and local lifecycle |
|
||||
| `app/domain/context.py` | `Context`, `MediaInfo`, and `TorrentInfo` |
|
||||
| `app/security/url.py` | URL/path validation, SSRF protection, and signed image URL policy |
|
||||
| `app/services/filter.py` | Persistent user filter-rule lookup and media-context selection |
|
||||
| `app/services/recognition.py` | Persistent recognition-rule lookup for domain injection |
|
||||
| `app/services/mediaserver.py` | Configured media-server discovery, Provider ID normalization, and music-library matching |
|
||||
| `app/startup/` | Runtime composition root |
|
||||
| `app/compat/manifest.py` | Exact legacy-to-canonical import manifest |
|
||||
| `app/sdk/` | Stable plugin imports |
|
||||
| `database/versions/` | Alembic migrations |
|
||||
|
||||
## Where New Capabilities Go
|
||||
|
||||
| Scenario | Action |
|
||||
|---|---|
|
||||
| New business workflow shared by multiple entrypoints | `app/chain/` |
|
||||
| New downloader, media server, message channel, or storage backend | `app/modules/<backend>/` |
|
||||
| New public HTTP API endpoint | `app/api/endpoints/`, register in `app/api/apiv1.py` |
|
||||
| New low-level utility reused in multiple places | `app/helper/` |
|
||||
| New deployment/env/startup config (ports, paths, API keys) | `ConfigModel` in `app/core/config.py` |
|
||||
| New runtime business config, user-editable rule, or persistent system option | `SystemConfigKey` + `SystemConfigOper` |
|
||||
| Config change should reload a long-lived object | Add `CONFIG_WATCH` + `on_config_changed()` to the relevant class |
|
||||
| Few dozen lines of private logic in one chain or module | Private function in the same file; do not create a new helper |
|
||||
| New module category or subtype | Also update `app/schemas/types.py` |
|
||||
| Shared business workflow | `app/chain/` |
|
||||
| Stateful focused application behavior | `app/services/` or the owning capability package |
|
||||
| New backend implementation | `app/modules/<backend>/` or `app/integrations/` |
|
||||
| New public HTTP endpoint | `app/api/endpoints/`, registered in `app/api/apiv1.py` |
|
||||
| Generic primitive, protocol client, or reflection mechanism | `app/foundation/` |
|
||||
| Media-domain parsing or rule | `app/domain/` |
|
||||
| Configuration-aware network, filesystem, process, feed, or generated resource adapter | `app/infrastructure/` |
|
||||
| Concrete third-party product or ecosystem integration | `app/integrations/` |
|
||||
| Deployment/startup setting | `ConfigModel` in `app/platform/config.py` |
|
||||
| Runtime user-editable option | `SystemConfigKey` plus `SystemConfigOper` |
|
||||
| New supported plugin API | Curated export in `app/sdk/` with compatibility tests |
|
||||
|
||||
*Last Updated: 2026-06-23*
|
||||
Run `tests/test_architecture_dependencies.py` after every ownership or import
|
||||
change. It rejects physical legacy sources, host legacy imports, implementation
|
||||
dependencies on SDK/compat, and any strongly connected component containing a
|
||||
canonical migrated module.
|
||||
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- All request body and response models must be defined as Pydantic `BaseModel` subclasses in `app/schemas/`.
|
||||
- Use `Field(...)` for required fields; use `Field(default=...)` or `Field(None)` for optional fields.
|
||||
- Do not define ad-hoc `dict` return types for API responses — define a schema class.
|
||||
- Settings and deployment configuration live in `ConfigModel` / `Settings` in `app/core/config.py` using `pydantic-settings`.
|
||||
- Settings and deployment configuration live in `ConfigModel` / `Settings` in `app/platform/config.py` using `pydantic-settings`.
|
||||
- Use `model_validator` for cross-field validation logic.
|
||||
|
||||
---
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
- Prefer `async def` for I/O-bound operations (network requests, database queries, file operations).
|
||||
- Use `await` consistently; do not mix sync and async code paths in the same function without using `run_in_threadpool` from FastAPI or `asyncio.to_thread`.
|
||||
- For CPU-bound work that must not block the event loop, submit to `ThreadHelper` (see `app/helper/thread.py`).
|
||||
- For CPU-bound work that must not block the event loop, submit to `ThreadHelper` (see `app/platform/thread.py`).
|
||||
- Do not use bare `threading.Thread` in new code; use `ThreadHelper.submit()`.
|
||||
|
||||
---
|
||||
@@ -62,7 +62,7 @@ Within each group, sort alphabetically. Do not use wildcard imports (`from modul
|
||||
|
||||
- In **chain and module layers**: do not raise HTTP exceptions. Catch exceptions, log them, and return `None` or a domain-level error object so the caller can decide how to proceed.
|
||||
- In **endpoint layer**: use FastAPI's `HTTPException` or the project's standard response schemas for errors.
|
||||
- Never swallow exceptions silently. At minimum log the error with `logger.error(f"...: {str(err)}")`.
|
||||
- Application and adapter layers must not swallow operational failures silently. Log or re-raise them according to the owning contract. Foundation primitives do not log; they return their documented fallback value or raise, leaving operational reporting to the caller.
|
||||
- Do not use bare `except:` — always catch a specific exception type or at minimum `Exception`.
|
||||
|
||||
```python
|
||||
@@ -84,7 +84,7 @@ except:
|
||||
|
||||
## Logging
|
||||
|
||||
- Use `logger` from `app/log.py`. Do not import the standard library `logging` directly in application code.
|
||||
- Host code uses `logger` from `app.platform.log`; new plugins use `app.sdk.logging`. The historical `app.log` path is compatibility-only. Do not import the standard library `logging` directly in application code.
|
||||
- Log levels:
|
||||
- `logger.debug(...)` — detailed diagnostic information, disabled by default.
|
||||
- `logger.info(...)` — normal operational events.
|
||||
@@ -103,10 +103,11 @@ except:
|
||||
|
||||
## File Organization
|
||||
|
||||
- One primary class per file is the norm for chains, modules, and helpers.
|
||||
- Private helper functions in the same file are preferable to extracting a new helper for single-use logic.
|
||||
- Under `app/api/endpoints/`, `app/chain/`, `app/helper/`, and `app/utils/`, add code to an existing domain file whenever the domain already exists.
|
||||
- New files under those directories must use a single noun filename such as `package.py`; avoid role-suffix names such as `package_installer.py` unless an established framework convention requires it.
|
||||
- One primary class per file is the norm for chains, modules, services, and adapters.
|
||||
- Private functions in the same file are preferable to extracting a new module for single-use logic.
|
||||
- Add code to the canonical capability package that owns it, and extend an existing domain file whenever that domain already exists.
|
||||
- Do not recreate generic `core`, `helper`, or `utils` buckets; see `05-architecture.md` for placement rules.
|
||||
- New files should use a focused noun name; a role suffix is appropriate only when it distinguishes ownership, such as `plugin_manager.py`; otherwise prefer the package-owned noun, such as `infrastructure/package.py`.
|
||||
- Keep files focused on one domain concern.
|
||||
|
||||
---
|
||||
@@ -114,10 +115,10 @@ except:
|
||||
## What Not To Do
|
||||
|
||||
- Do not introduce new third-party libraries without placing them in the correct dependency entry: runtime packages in `requirements.in`, test/lint/build tooling in `requirements-dev.in`.
|
||||
- Do not use `requests` or `httpx` directly for external HTTP calls — use `RequestUtils` from `app/utils/http.py`.
|
||||
- Do not use `requests` or `httpx` directly for external HTTP calls - host code uses `RequestUtils` from `app/foundation/http.py`; plugins use `app.sdk.network`.
|
||||
- Do not issue raw SQLAlchemy queries from chains, modules, or endpoints — use the `*_oper.py` classes.
|
||||
- Do not add TODO or FIXME without context. Only keep one if it is genuinely deferred and cannot be addressed in the current task.
|
||||
- Do not add noisy markers like `# change starts here`, `# important`, or `# this is a fix`.
|
||||
- Do not write comments that restate what the code already clearly says.
|
||||
|
||||
*Last Updated: 2026-06-23*
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -9,7 +9,7 @@ All new code must follow these conventions. Consistent naming is how the codebas
|
||||
| Context | Convention | Examples |
|
||||
|---|---|---|
|
||||
| Python source files | `snake_case.py` | `download.py`, `qbittorrent.py`, `package.py` |
|
||||
| New domain files under `app/api/endpoints/`, `app/chain/`, `app/helper/`, `app/utils/` | Single noun `snake_case.py`; prefer an existing domain file before adding a new one | `package.py`, `plugin.py`, `torrent.py` |
|
||||
| New files in canonical capability packages | Focused `snake_case.py`; prefer a package-owned noun and an existing owned domain file before adding one | `torrent.py`, `plugin_manager.py`, `package.py` |
|
||||
| Module package directories | `snake_case/` | `qbittorrent/`, `synologychat/` |
|
||||
| Test files | `test_<domain>.py` | `test_download_chain.py`, `test_subscribe_endpoint.py` |
|
||||
| Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## HTTP Client Conventions
|
||||
|
||||
**Rule:** All outbound HTTP requests must go through `RequestUtils` from `app/utils/http.py`. Do not use `requests`, `httpx`, or `aiohttp` directly.
|
||||
**Rule:** Host outbound HTTP requests must go through `RequestUtils` from `app/foundation/http.py`. Plugins import it from `app.sdk.network`. Do not use `requests`, `httpx`, or `aiohttp` directly.
|
||||
|
||||
`RequestUtils` handles:
|
||||
- Proxy configuration (from `settings.PROXY_*`)
|
||||
@@ -12,7 +12,7 @@
|
||||
- Retry logic
|
||||
|
||||
```python
|
||||
from app.utils.http import RequestUtils
|
||||
from app.foundation.http import RequestUtils
|
||||
|
||||
res = RequestUtils(
|
||||
ua=settings.USER_AGENT,
|
||||
@@ -136,7 +136,7 @@ Internal notifications use the `Notification` schema and the event system:
|
||||
```python
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import NotificationType, MessageChannel
|
||||
from app.core.event import eventmanager
|
||||
from app.platform.events import eventmanager
|
||||
from app.schemas.types import EventType
|
||||
|
||||
eventmanager.send_event(
|
||||
@@ -171,4 +171,4 @@ Webhook payloads arrive at `app/api/endpoints/webhook.py` and are dispatched via
|
||||
|
||||
Do not add webhook-specific business logic directly in the endpoint. The endpoint parses the payload and fires the event; the chain handles the response.
|
||||
|
||||
*Last Updated: 2026-05-25*
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -120,14 +120,14 @@ oper.set(user_id=1, key="notification_enabled", value=True)
|
||||
|
||||
**Purpose:** Deployment-level, environment-level, and startup-time configuration such as ports, paths, proxies, switches, API keys, and third-party service addresses.
|
||||
|
||||
**Location:** `ConfigModel` and `Settings` in `app/core/config.py`
|
||||
**Location:** `ConfigModel` and `Settings` in `app/platform/config.py`
|
||||
|
||||
These values are read from environment variables (or `.moviepilot.env`) at startup and are immutable at runtime. They are not stored in the database.
|
||||
|
||||
**Access:**
|
||||
|
||||
```python
|
||||
from app.core.config import settings
|
||||
from app.platform.config import settings
|
||||
|
||||
host = settings.QB_HOST
|
||||
port = settings.QB_PORT
|
||||
@@ -139,12 +139,12 @@ port = settings.QB_PORT
|
||||
|
||||
### FileCache / AsyncFileCache
|
||||
|
||||
**Location:** `app/core/cache.py`
|
||||
**Location:** `app/platform/cache.py`
|
||||
|
||||
Used to cache expensive external API responses to disk. Cache entries have a configurable TTL.
|
||||
|
||||
```python
|
||||
from app.core.cache import FileCache, fresh
|
||||
from app.platform.cache import FileCache, fresh
|
||||
|
||||
cache = FileCache(cache_name="tmdb", ttl=3600)
|
||||
|
||||
@@ -174,4 +174,4 @@ When `REDIS_HOST` is configured, `app/modules/redis/` provides a distributed cac
|
||||
- `settings.API_TOKEN` and other secret fields must not be included in log output or API responses.
|
||||
- The `config list --show-secrets` flag exists specifically to gate secret visibility in the CLI.
|
||||
|
||||
*Last Updated: 2026-05-25*
|
||||
*Last Updated: 2026-08-14*
|
||||
|
||||
@@ -15,7 +15,7 @@ pytest
|
||||
### When to Expand Scope
|
||||
|
||||
Run the full test suite when changing:
|
||||
- `app/core/` — config, event system, module manager, plugin manager
|
||||
- `app/platform/`, `app/extensions/`, or `app/compat/` - config, events, managers, and compatibility boundaries
|
||||
- `app/chain/__init__.py` — chain base class
|
||||
- `app/modules/__init__.py` — module base class
|
||||
- `app/main.py` — application startup
|
||||
|
||||
Reference in New Issue
Block a user