Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
152e008
docs(plans): add Stage C (STE100 sweep) and the Phase 7 pre-flight audit
nandyalu Aug 28, 2026
8dd6542
refactor(phase7): move the database layer to backend/database
nandyalu Aug 28, 2026
a9310bf
refactor(phase7): move core/updates to services/updates
nandyalu Aug 28, 2026
3dbf272
refactor(phase7): move core/notifications to services/notifications
nandyalu Aug 28, 2026
f2d0a3b
refactor(phase7): move core/binaries.py to services/binaries.py
nandyalu Aug 28, 2026
2475aa7
refactor(phase7): move core/diagnostics to services/diagnostics
nandyalu Aug 28, 2026
68a2a45
refactor(phase7): move profiles, filters and satisfaction to services
nandyalu Aug 28, 2026
7662900
refactor(phase7): add a utils layer and move path_utils into it
nandyalu Aug 28, 2026
d1b828a
refactor(phase7): move files_handler and the media scanner to service…
nandyalu Aug 28, 2026
fb8661d
refactor(phase7): move core/plex to services/connections/plex
nandyalu Aug 28, 2026
a12ef26
refactor(phase7): move the Arr integrations to services/connections/arr
nandyalu Aug 28, 2026
e69febb
refactor(phase7): move connection_manager to services/connections/base
nandyalu Aug 28, 2026
c086987
refactor(phase7): split core/download into trailers, images and utils
nandyalu Aug 28, 2026
fc204f1
refactor(phase7): move core/tasks to tasks and retire the core package
nandyalu Aug 28, 2026
05021c7
docs: record Stage A completion and update the architecture map
nandyalu Aug 28, 2026
1487df8
docs: correct the move map so path_utils and error_classify show unde…
nandyalu Aug 28, 2026
2a9da1a
refactor(phase7): move connection probing out of the database layer
nandyalu Aug 29, 2026
5dbde88
refactor(phase7): let services subscribe to events instead of being i…
nandyalu Aug 29, 2026
71b3f59
docs: record the Stage B layering work as done
nandyalu Aug 29, 2026
a5d2d74
refactor(phase7): move the auth logic to services/auth.py
nandyalu Aug 29, 2026
9be67f4
refactor(phase7): thin the settings router
nandyalu Aug 29, 2026
e47db4f
refactor(phase7): thin the logs router
nandyalu Aug 29, 2026
db0a6cc
refactor(phase7): thin the files router
nandyalu Aug 29, 2026
1d71992
refactor(phase7): thin the connections router
nandyalu Aug 29, 2026
5bbd374
refactor(phase7): thin the three heaviest media handlers
nandyalu Aug 29, 2026
b923974
docs: record Stage B API-thinning progress
nandyalu Aug 29, 2026
3da5e13
docs: record the is_path_safe problems as hygiene H13
nandyalu Aug 29, 2026
02e3b09
refactor(phase7): route the media batch path through the service
nandyalu Aug 29, 2026
c522286
refactor(phase7): stop handlers answering 404 with the exception text…
nandyalu Aug 29, 2026
23543e6
docs(api): document the 500 responses the handlers can return (H1)
nandyalu Aug 29, 2026
01d22c0
fix(phase7): give connections.py the logger the error mapper needs
nandyalu Aug 29, 2026
9aa0935
docs: mark H1 done and Stage B complete
nandyalu Aug 29, 2026
8f26caa
fix(logs): let a log line say which media it is about
nandyalu Aug 29, 2026
7932dd2
docs: set one house style for log messages
nandyalu Aug 29, 2026
42dd895
refactor(phase7): rewrite the log messages in tasks/ (Stage C)
nandyalu Aug 29, 2026
48dac28
refactor(phase7): rewrite the log messages in services/trailers (Stag…
nandyalu Aug 29, 2026
d72df23
refactor(phase7): rewrite the remaining log messages (Stage C)
nandyalu Aug 29, 2026
1a0ce39
refactor(phase7): finish the log message sweep (Stage C)
nandyalu Aug 29, 2026
fa1ff60
docs(api): rewrite three route descriptions (Stage C spec diff)
nandyalu Aug 29, 2026
ed9f76c
docs: record what the Stage C log sweep found
nandyalu Aug 29, 2026
0a4f3a7
docs: point the contributor instructions at the new backend layout
nandyalu Aug 29, 2026
dd4a050
refactor(phase7): fold helpers into shared, and document the app layout
nandyalu Aug 29, 2026
1124423
docs: point the prose in the code at the new paths
nandyalu Aug 29, 2026
9a64be3
fix(phase7): the last bracket-style media ids, and a guard for the mi…
nandyalu Aug 29, 2026
3b1a8b7
docs: record the Phase 7 verification protocol
nandyalu Aug 29, 2026
1cda180
docs(phase7): say what each module is for
nandyalu Aug 29, 2026
fd5e8d4
docs(phase7): say what each api, config and database module is for
nandyalu Aug 29, 2026
690d895
docs(phase7): every backend module now says what it is for
nandyalu Aug 29, 2026
05a7d7d
docs: mark Phase 7 done
nandyalu Aug 29, 2026
b01f99c
docs: record what Phase 7 found and did not fix
nandyalu Aug 30, 2026
f4d0b60
docs: put the two Phase 7 lessons where they will be read
nandyalu Aug 30, 2026
88f727c
fix(phase7): keep the released path-guard fix through the rebase
nandyalu Aug 30, 2026
6988772
Merge dev (v0.11.5) into the Phase 7 reorg
nandyalu Sep 3, 2026
ab09fcb
docs(phase7): record how this phase lands in dev
nandyalu Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
101 changes: 56 additions & 45 deletions .github/instructions/backend.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,32 @@ backend/
│ ├── logging_context.py # Logging context utilities
│ └── timing_middleware.py # Request timing middleware
├── core/ # Core business logic
│ ├── files_handler.py # File operations handler (needs to be break down and move to files package)
│ ├── base/ # Shared base components
│ │ ├── database/ # Database layer
│ │ │ ├── models/ # SQLModel table definitions
│ │ │ ├── manager/ # CRUD operations per model
│ │ │ └── utils/ # DB engine, session management
│ │ ├── arr_manager/ # Base Arr API client
│ │ ├── connection_manager.py # Arr Connection manager (refresh data)
│ │ └── utils/ # Shared utilities
│ ├── radarr/ # Radarr-specific logic
│ ├── sonarr/ # Sonarr-specific logic
│ ├── download/ # download logic (images, trailers)
│ ├── files/ # File management
│ ├── tasks/ # Background task definitions
│ └── updates/ # Update checking logic
├── services/ # Business logic
│ ├── connections/ # Arr and Plex integrations
│ │ ├── base.py # BaseConnectionManager (shared refresh, CRUD)
│ │ ├── probe.py # Network probes against a server
│ │ ├── service.py # Create and update a connection
│ │ ├── arr/ # Base Arr client, with radarr/ and sonarr/
│ │ └── plex/ # Plex client, parser and models
│ ├── trailers/ # Trailer download, search and conversion
│ ├── files/ # File operations and the media scanner
│ ├── diagnostics/ # Connection Doctor, health checks, cookies
│ ├── images/ # Image download
│ ├── notifications/ # Apprise dispatch
│ ├── updates/ # Update checking logic
│ ├── auth.py # Sessions and credentials
│ ├── media.py # Actions on a media item
│ └── profiles.py, filters.py, satisfaction.py, settings.py, logs.py
├── database/ # Persistence layer
│ ├── models/ # SQLModel table definitions
│ ├── manager/ # CRUD operations per model
│ └── engine.py, init_db.py, version_guard.py
├── tasks/ # Background task definitions and scheduling
├── utils/ # Pure helpers, no dependency on other layers
│ └── path_utils.py, error_classify.py
├── alembic/ # Database migrations
│ ├── env.py # Alembic environment config
Expand All @@ -71,8 +81,10 @@ backend/
├── conftest.py # Pytest fixtures and configuration
├── api/ # API route tests
├── config/ # Configuration tests
├── core/ # Core logic tests
└── services/ # Service tests
├── database/ # Database layer tests
├── services/ # Service tests
├── tasks/ # Background task tests
└── utils/ # Helper tests
```

## Core Patterns
Expand All @@ -81,7 +93,7 @@ backend/

The database layer follows a **Model → Manager → API** pattern:

#### Models (`core/base/database/models/`)
#### Models (`database/models/`)

Define SQLModel classes with clear separation:

Expand Down Expand Up @@ -114,7 +126,7 @@ class MediaUpdate(SQLModel):
title: str | None = None
```

#### Managers (`core/base/database/manager/`)
#### Managers (`database/manager/`)

- Always fetch db model of resource and return read model, db model should never leave manager
- Should be short operations to prevent db lockup
Expand All @@ -137,7 +149,7 @@ __all__ = ["create_or_update_bulk", "read", "read_all", ...]
Each operation file uses the session decorator:

```python
from core.base.database.utils.engine import manage_session
from database.engine import manage_session

@manage_session
def read(
Expand Down Expand Up @@ -169,8 +181,8 @@ Routes are organized by resource in `api/v1/`:
from fastapi import APIRouter, HTTPException, status
from api.v1.models import ErrorResponse
from app_logger import ModuleLogger
import core.base.database.manager.resource as resource_manager
from core.base.database.models.resource import ResourceCreate, ResourceRead
import database.manager.resource as resource_manager
from database.models.resource import ResourceCreate, ResourceRead

logger = ModuleLogger("ResourceAPI")

Expand Down Expand Up @@ -269,10 +281,10 @@ async def get_item(item_id: int) -> ItemRead:

### 5. Background Tasks Pattern

Tasks are using `apscheduler`, defined in `core/tasks/` and scheduled in `main.py`:
Tasks are using `apscheduler`, defined in `tasks/` and scheduled in `main.py`:

```python
# core/tasks/my_task.py
# tasks/my_task.py
from app_logger import ModuleLogger

logger = ModuleLogger("MyTask")
Expand All @@ -283,9 +295,9 @@ def my_background_job():
# ... task logic
logger.info("Background job completed")

# core/tasks/schedules.py
from core.tasks import scheduler
from core.tasks.my_task import my_background_job
# tasks/schedules.py
from tasks import scheduler
from tasks.my_task import my_background_job

def schedule_all_tasks():
scheduler.add_job(
Expand Down Expand Up @@ -317,7 +329,7 @@ uv run alembic history

### Creating a New Migration

1. **Modify the SQLModel** in `core/base/database/models/`
1. **Modify the SQLModel** in `database/models/`

2. **Generate migration** using the Alembic CLI (preferred):

Expand Down Expand Up @@ -393,19 +405,18 @@ tests/
├── conftest.py # Shared fixtures, temp DB setup
├── api/ # API endpoint tests
├── config/ # Configuration tests
├── core/
│ ├── base/
│ │ └── database/ # Database operation tests
│ └── download/ # Download logic tests
└── services/ # Service integration tests
├── database/ # Database operation tests
├── services/ # Service tests, mirroring services/
├── tasks/ # Background task tests
└── utils/ # Helper tests
```

### Writing Tests

```python
import pytest
from core.base.database.manager import resource as resource_manager
from core.base.database.models.resource import ResourceCreate
from database.manager import resource as resource_manager
from database.models.resource import ResourceCreate

class TestResourceManager:
"""Tests for resource manager operations."""
Expand Down Expand Up @@ -440,19 +451,19 @@ uv run python -m pytest --cov=. --cov-report=html

### Adding a New API Endpoint

1. **Create/update models** in `core/base/database/models/[resource].py`
2. **Create/update manager** functions in `core/base/database/manager/[resource]/`
1. **Create/update models** in `database/models/[resource].py`
2. **Create/update manager** functions in `database/manager/[resource]/`
3. **Add route handler** in `api/v1/[resource].py`
4. **Register router** in `api/v1/routes.py` (if new file)
5. **Add tests** in `tests/api/` and `tests/core/`
5. **Add tests** in `tests/api/` and the folder that mirrors the code
6. **Regenerate OpenAPI** - Run VSCode task "Generate OpenAPI Files"
7. **Update frontend API client** - Run `npm run openapi` in frontend

### Adding a New Database Model

1. **Create model file** in `core/base/database/models/[model].py`
2. **Export from** `core/base/database/models/__init__.py`
3. **Create manager folder** `core/base/database/manager/[model]/`
1. **Create model file** in `database/models/[model].py`
2. **Export from** `database/models/__init__.py`
3. **Create manager folder** `database/manager/[model]/`
4. **Add CRUD operations** (create.py, read.py, update.py, delete.py)
5. **Export from manager** `__init__.py`
6. **Create migration**:
Expand All @@ -464,9 +475,9 @@ uv run python -m pytest --cov=. --cov-report=html

### Adding a New Background Task

1. **Create task file** in `core/tasks/[task_name].py`
2. **Add schedule** in `core/tasks/schedules.py`
3. **Add tests** in `tests/core/tasks/`
1. **Create task file** in `tasks/[task_name].py`
2. **Add schedule** in `tasks/schedules.py`
3. **Add tests** in `tests/tasks/`

## Documentation Requirements

Expand Down
46 changes: 23 additions & 23 deletions .github/planned_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ Send notifications to a user-configured Discord webhook channel when configured

**Design:**
- Store webhook URL + per-event-type toggle in `app_settings` (`config/settings.py`). Use a JSON blob (cleaner since `EventType` is extensible) rather than individual bool properties.
- Hook point: `core/base/database/manager/event/create.py` — after an event is saved, fire the notification. Or hook into wherever events are created in the download flow.
- New file: `core/notifications/discord.py` — `async def send_discord_notification(event: EventCreate, media: MediaRead) -> None` — POSTs to the webhook URL with a Discord embed payload.
- Existing `EventType` values in `core/base/database/models/event.py` map directly to notification triggers: `TRAILER_DOWNLOADED`, `TRAILER_DELETED`, `MEDIA_ADDED`, etc.
- Hook point: `database/manager/event/create.py` — after an event is saved, fire the notification. Or hook into wherever events are created in the download flow.
- New file: `services/notifications/discord.py` — `async def send_discord_notification(event: EventCreate, media: MediaRead) -> None` — POSTs to the webhook URL with a Discord embed payload.
- Existing `EventType` values in `database/models/event.py` map directly to notification triggers: `TRAILER_DOWNLOADED`, `TRAILER_DELETED`, `MEDIA_ADDED`, etc.

**Frontend:**
- New settings section: webhook URL input + checkboxes per event type.
Expand All @@ -34,12 +34,12 @@ Send notifications to a user-configured Discord webhook channel when configured
Create Kodi/Jellyfin/Emby-compatible `.nfo` files alongside downloaded trailers.

**Schema change:**
- Add `create_nfo: bool = False` to `TrailerProfile` (`core/base/database/models/trailerprofile.py`).
- Add `create_nfo: bool = False` to `TrailerProfile` (`database/models/trailerprofile.py`).
- Requires a new Alembic migration.

**Implementation:**
- New utility: `core/files/nfo_writer.py` — takes `MediaRead` + trailer file path, writes `{filename}.nfo` with standard XML (title, year, overview, imdbid, tmdbid, studio, etc.).
- Write point: after the trailer file is saved in the download completion flow (`core/download/trailers/service.py` or `FilesHandler`), if `profile.create_nfo` is True, call the NFO writer.
- New utility: `services/files/nfo_writer.py` — takes `MediaRead` + trailer file path, writes `{filename}.nfo` with standard XML (title, year, overview, imdbid, tmdbid, studio, etc.).
- Write point: after the trailer file is saved in the download completion flow (`services/trailers/trailers/service.py` or `FilesHandler`), if `profile.create_nfo` is True, call the NFO writer.

**Naming convention:** NFO filename must exactly match the trailer filename (minus extension) to be picked up by media servers — e.g., `Movie (2023)-trailer.nfo` alongside `Movie (2023)-trailer.mkv`. This follows the existing `file_name` template in `TrailerProfile`.

Expand All @@ -52,9 +52,9 @@ Create Kodi/Jellyfin/Emby-compatible `.nfo` files alongside downloaded trailers.
Add Plex as a connector alongside Radarr/Sonarr. Can work standalone (no Arr apps needed) or alongside them (enriches existing media items). Triggers Plex library scans after trailer download/delete.

**Prior work:** Branch `copilot/fix-87efcdd2-f25b-409c-9e92-81e7ff32f57e` + stashes named `plex` contain early-phase work. Key reusable pieces from the stashes:
- `core/plex/api.py` — `PlexAPI` class (clean async wrapper, keep with fixed imports)
- `core/plex/models.py` — `PlexLibrarySection`, `PlexMediaItem`, `PlexMediaExtra` Pydantic models (keep as-is)
- `core/plex/auth.py` — `start_auth_flow()`, `poll_for_token()`, `get_server_address()` logic → **port to frontend TypeScript**, do not keep as backend endpoints
- `services/connections/plex/api.py` — `PlexAPI` class (clean async wrapper, keep with fixed imports)
- `services/connections/plex/models.py` — `PlexLibrarySection`, `PlexMediaItem`, `PlexMediaExtra` Pydantic models (keep as-is)
- `services/connections/plex/auth.py` — `start_auth_flow()`, `poll_for_token()`, `get_server_address()` logic → **port to frontend TypeScript**, do not keep as backend endpoints
- Debug JSON files (`debug_media_items_*.json`, `extras.json`, etc.) — **do not commit**

---
Expand All @@ -63,12 +63,12 @@ Add Plex as a connector alongside Radarr/Sonarr. Can work standalone (no Arr app

#### Schema Changes

**`core/base/database/models/connection.py`:**
**`database/models/connection.py`:**
- Add `PLEX = "plex"` to `ArrType` enum.
- `url` holds the Plex server address; `api_key` holds the Plex token. No new columns on `Connection`.
- Alembic migration required.

**`core/base/database/models/media.py` — three new nullable columns:**
**`database/models/media.py` — three new nullable columns:**
- `plex_rating_key: str | None = None` — Plex `ratingKey` for the item; used for per-item scan/extras calls.
- `plex_section_key: str | None = None` — Plex library section key; used for section-level scan.
- `plex_connection_id: int | None` — nullable FK to `Connection.id` (with `ondelete="SET NULL"`). Present on ALL media items (Arr and Plex-only alike) so the library scan trigger always knows which Plex server to call.
Expand All @@ -81,9 +81,9 @@ Add Plex as a connector alongside Radarr/Sonarr. Can work standalone (no Arr app

#### New Files

- `core/plex/api_manager.py` — rename/adapt `core/plex/api.py` from stash. Fix `from models import` → `from core.plex.models import`. This is the equivalent of `RadarrManager`/`SonarrManager`.
- `core/plex/data_parser.py` — `parse_plex_item(item: PlexMediaItem, connection_id: int) -> MediaCreate`. Maps `PlexMediaItem` fields to `MediaCreate`. `arr_id=0`, `plex_rating_key=item.ratingKey`, `plex_section_key` from the library section, `txdb_id` from `item.tmdb_id`, `imdb_id` from `item.imdb_id`, etc.
- `core/plex/connection_manager.py` — `PlexConnectionManager`. `refresh()` logic:
- `services/connections/plex/api_manager.py` — rename/adapt `services/connections/plex/api.py` from stash. Fix `from models import` → `from core.plex.models import`. This is the equivalent of `RadarrManager`/`SonarrManager`.
- `services/connections/plex/data_parser.py` — `parse_plex_item(item: PlexMediaItem, connection_id: int) -> MediaCreate`. Maps `PlexMediaItem` fields to `MediaCreate`. `arr_id=0`, `plex_rating_key=item.ratingKey`, `plex_section_key` from the library section, `txdb_id` from `item.tmdb_id`, `imdb_id` from `item.imdb_id`, etc.
- `services/connections/plex/connection_manager.py` — `PlexConnectionManager`. `refresh()` logic:
1. Call `PlexAPI.get_libraries()` to get all sections.
2. For each section, call `PlexAPI.get_library_media(section_key)` to get items.
3. Apply path mappings (same as Arr: translate Plex-side paths to Trailarr-side paths).
Expand All @@ -93,9 +93,9 @@ Add Plex as a connector alongside Radarr/Sonarr. Can work standalone (no Arr app

#### Existing Files to Modify

- `core/base/database/manager/connection/base.py` — add `PLEX` branch to `validate_connection()` (calls `PlexAPI.validate_token()`) and `get_rootfolders()` (calls `PlexAPI.get_library_folders()` — returns the `Location` paths from all sections, same role as Arr rootfolders).
- `core/tasks/api_refresh.py` — add `PLEX` branch to `api_refresh_by_id()` to instantiate `PlexConnectionManager`.
- `core/download/trailers/` (download completion) — after trailer download/delete, if `media.plex_connection_id` is set, look up that connection, instantiate `PlexAPI`, call `PlexAPI` section or item refresh. Controlled by `notify_plex` on `TrailerProfile` (field already exists).
- `database/manager/connection/base.py` — add `PLEX` branch to `validate_connection()` (calls `PlexAPI.validate_token()`) and `get_rootfolders()` (calls `PlexAPI.get_library_folders()` — returns the `Location` paths from all sections, same role as Arr rootfolders).
- `tasks/api_refresh.py` — add `PLEX` branch to `api_refresh_by_id()` to instantiate `PlexConnectionManager`.
- `services/trailers/trailers/` (download completion) — after trailer download/delete, if `media.plex_connection_id` is set, look up that connection, instantiate `PlexAPI`, call `PlexAPI` section or item refresh. Controlled by `notify_plex` on `TrailerProfile` (field already exists).

#### Library Scan Trigger

Expand Down Expand Up @@ -168,10 +168,10 @@ Decouple the app from Arr apps entirely — scan folders, resolve metadata from

### Sub-task A: TMDB Integration (prerequisite)

- New file: `core/tmdb/api_manager.py` — wraps TMDB v3 REST API.
- New file: `services/tmdb/api_manager.py` — wraps TMDB v3 REST API.
- Key endpoints: `/search/movie`, `/search/tv`, `/movie/{id}`, `/tv/{id}`, `/movie/{id}/images`.
- API key stored in `app_settings` (global, not per-connection).
- The `txdb_id` field on `Media` (`core/base/database/models/media.py`) is the correct slot for TMDB IDs.
- The `txdb_id` field on `Media` (`database/models/media.py`) is the correct slot for TMDB IDs.

### Sub-task B: Filesystem Connector

Expand All @@ -180,12 +180,12 @@ Decouple the app from Arr apps entirely — scan folders, resolve metadata from
This approach is preferred over creating a separate concept because it leverages all existing connection/media/manager/filtering/download infrastructure without modification.

**New files:**
- `core/filesystem/connection_manager.py` — `refresh()` walks the folder path, identifies movie/series folders (by year-in-name pattern etc.), calls TMDB to resolve metadata, upserts `MediaCreate` into DB.
- `core/filesystem/data_parser.py` — converts TMDB API response + local path into `MediaCreate`.
- `services/connections/filesystem/connection_manager.py` — `refresh()` walks the folder path, identifies movie/series folders (by year-in-name pattern etc.), calls TMDB to resolve metadata, upserts `MediaCreate` into DB.
- `services/connections/filesystem/data_parser.py` — converts TMDB API response + local path into `MediaCreate`.

**Existing files to modify:**
- `core/base/database/manager/connection/base.py` — add `FILESYSTEM` branch to `validate_connection()` (checks path exists + TMDB API key is configured) and `get_rootfolders()` (returns configured path).
- `core/tasks/api_refresh.py` — add `FILESYSTEM` branch to instantiate `FilesystemConnectionManager`.
- `database/manager/connection/base.py` — add `FILESYSTEM` branch to `validate_connection()` (checks path exists + TMDB API key is configured) and `get_rootfolders()` (returns configured path).
- `tasks/api_refresh.py` — add `FILESYSTEM` branch to instantiate `FilesystemConnectionManager`.

**Edge cases:**
- `arr_id` (FK-like integer from Arr apps) → use TMDB integer ID.
Expand Down
Loading
Loading