Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions .planning/codebase/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Architecture

**Analysis Date:** 2026-01-24

## Pattern Overview

**Overall:** Plex Agent Plugin Pattern

This is a single-purpose Plex agent plugin that implements the `Agent.Movies` class interface. It follows Plex's plugin architecture where agents are dynamically loaded and invoked by the Plex Media Server to enrich metadata for specific media types. The agent fetches scene metadata from a remote Stash instance via GraphQL and populates Plex metadata fields.

**Key Characteristics:**
- Single monolithic module (`Contents/Code/__init__.py`) containing all agent logic
- Entry point driven by Plex framework callbacks (`search()` and `update()` methods)
- Stateless request/response pattern for GraphQL queries to Stash
- Preference-driven configuration using Plex's preference system
- Bidirectional sync capability (pulls from Stash, optionally writes back Plex ratings)

## Layers

**Framework Integration Layer:**
- Purpose: Bridge between Plex framework and agent logic
- Location: `Contents/Code/__init__.py` (lines 146-183 for search, 185-535 for update)
- Contains: `StashPlexAgent` class inheriting from `Agent.Movies`
- Depends on: Plex built-in modules (`Agent`, `Prefs`, `HTTP`, `JSON`, `String`, `Locale`, `Proxy`, `MetadataSearchResult`)
- Used by: Plex Media Server runtime

**Configuration Layer:**
- Purpose: Load and validate user preferences
- Location: `Contents/DefaultPrefs.json` (default values), `Contents/Info.plist` (plugin metadata)
- Contains: 32 configurable preferences controlling behavior flags, prefixes, tag filtering, and connection parameters
- Depends on: Plex preference system
- Used by: All business logic functions

**API Communication Layer:**
- Purpose: Handle HTTP requests to remote Stash GraphQL endpoint
- Location: `Contents/Code/__init__.py` (lines 22-73: `HttpReq()`, `HttpPost()`)
- Contains: HTTP request construction with query parameter encoding, retry logic, debug logging
- Depends on: Plex `HTTP` module, Python `urllib`, `urllib2`, preferences (hostname, port, HTTPS flag, API key)
- Used by: `search()` and `update()` methods

**Data Processing Layer:**
- Purpose: Transform Stash metadata into Plex metadata objects
- Location: `Contents/Code/__init__.py` (lines 75-143 for `FormattedTitle()`, lines 232-535 for metadata population in `update()`)
- Contains: Title formatting, date parsing, tag filtering, collection tag creation, rating sync, image processing
- Depends on: API layer (for raw Stash data), preferences, Python `dateutil`, OS utilities
- Used by: `update()` method

## Data Flow

**Scene Search Flow:**

1. Plex calls `search(results, media, lang)` with media file information
2. Extract filename from `media.items[0].parts[0].file` and clean extension
3. Construct GraphQL query to search Stash by filename (path match or include-path match based on preference)
4. Call `HttpReq()` to execute query against Stash GraphQL endpoint
5. Parse response into `movie_data` list of matching scenes
6. For each scene: construct title (scene title + date, or fallback to filename)
7. Optionally apply `FormattedTitle()` transformation if preference enabled
8. Append `MetadataSearchResult` objects to results list with score (100 if exact match, 85 for multiple matches)

**Metadata Update Flow:**

1. Plex calls `update(metadata, media, lang, force)` with matched scene ID in `metadata.id`
2. Query Stash for complete scene data using the scene ID (full GraphQL query with 20+ fields)
3. Validate data against three optional pre-flight checks:
- `RequireOrganized`: Scene must be marked organized in Stash
- `RequireURL`: Scene must have URLs defined
- `RequireStashID`: Scene must have stash_ids defined
4. If validation fails, set `allow_scrape = False` and exit early
5. If data exists and validation passes, populate metadata fields:
- Title (with optional formatting)
- Release date and year (parsed via dateutil)
- Studio name
- Rating (converted from Stash 0-100 scale to 0-10)
- Summary/Details
- Collections (site, studio parent, movies, performers, tags based on preferences)
- Genres (from Stash tags, filtered by IgnoreTags)
- Content rating (hardcoded to "XXX")
- Performer roles with photos
- Posters (from scene screenshot and gallery images)
6. If `AddPlexURL` preference enabled: Create GraphQL mutation to append Plex URL back to Stash scene
7. If `SaveUserRatings` preference enabled: Make REST call to local Plex server to sync rating to user rating field

**State Management:**
- No persistent state; all state comes from request parameters and preferences
- Plex Media Server manages the matching between file and metadata result
- Stash database is the source of truth for scene metadata
- Optional back-sync from Plex ratings to Stash via GraphQL mutation

## Key Abstractions

**HTTP Request Wrapper (`HttpReq()` and `HttpPost()`):**
- Purpose: Abstract GraphQL query execution and error handling
- Examples: Lines 22-42 (GET), lines 44-73 (POST)
- Pattern: Construct full URL from preferences, append API key if configured, execute with retry on first failure
- Retry strategy: Failed request automatically retries once (lines 40-42, 71-73)
- Error handling: Catches exceptions, can raise on second failure or silently pass depending on retry flag

**Title Formatting (`FormattedTitle()`):**
- Purpose: Apply user-defined title template using Stash metadata
- Examples: Lines 75-143
- Pattern: Template substitution with performer filtering, tag-based performer exclusion, prefix removal
- Filters out performers: with missing names, tagged with IgnoreTags IDs
- Supports variables: `{performer}`, `{title}`, `{date}`, `{studio}`, `{filename}`

**Collection Tag Creation:**
- Purpose: Generate Plex collection tags from Stash metadata
- Pattern: Multiple conditional blocks (lines 331-401) create collections based on preferences:
- Site collections (from studio name)
- Studio collections (from parent studio)
- Movie collections (from associated movies)
- Performer/Actor collections (from scene performers)
- Tag collections (from scene tags and performer tags)
- Rating collections (from Stash 0-100 rating)
- Organized flag collection
- Each with optional custom prefix (configurable via preferences)
- All wrapped in try/except to prevent single tag failure from stopping metadata update

**Tag Filtering:**
- Purpose: Exclude specified tag IDs from genres and collections
- Pattern: Split comma-separated IgnoreTags preference, map to list of IDs, check with `if not genre["id"] in ignore_tags`
- Also filters out tags containing "ambiguous" in name (hardcoded, lines 420, 441)

## Entry Points

**`Start()`:**
- Location: `Contents/Code/__init__.py`, lines 16-20
- Triggers: Called once when Plex Media Server loads the agent plugin
- Responsibilities: Set HTTP headers to accept JSON, set cache time, call preferences validation

**`ValidatePrefs()`:**
- Location: `Contents/Code/__init__.py`, lines 13-14
- Triggers: Called by `Start()` when agent initializes
- Responsibilities: Currently a no-op; can be expanded to validate preference values

**`search(results, media, lang)`:**
- Location: `Contents/Code/__init__.py`, lines 155-182
- Triggers: Called by Plex when a media file is being matched to metadata (user clicks "Fix Match" or during automatic matching)
- Responsibilities:
- Extract filename from media object
- Query Stash for matching scenes
- Format results with title and score
- Return list of possible matches for user selection

**`update(metadata, media, lang, force)`:**
- Location: `Contents/Code/__init__.py`, lines 185-535
- Triggers: Called by Plex after user selects a search result or for metadata refresh
- Responsibilities:
- Fetch full scene details from Stash using matched ID
- Validate data against requirements
- Populate all Plex metadata fields (title, date, rating, summary, etc.)
- Create collections, genres, and performer roles
- Optionally sync back to Stash (ratings, Plex URL)

## Error Handling

**Strategy:** Defensive with silent failures

Errors are handled in two ways:
1. **Network/API errors:** Retry once on first failure (lines 40-42, 71-73). If retry fails, raise exception or return corrupted data.
2. **Data processing errors:** Wrapped in try/except blocks that silently pass, allowing partial metadata population even if some operations fail (examples: lines 221-224, 365-369, 415-452, 471-472, 512-523)

This approach ensures one bad field (e.g., gallery image load failure) doesn't prevent the entire metadata update from completing.

**Notable error handling patterns:**
- Lines 226-230: Handle case where Stash ID changed but Plex still references old ID
- Lines 337-347: Try/except around each individual collection tag addition
- Lines 452-453: Catch-all for entire tag processing block
- Lines 489-491: Log exception for poster creation but continue

## Cross-Cutting Concerns

**Logging:**
- Framework: Plex `Log()` function (built-in)
- Patterns:
- Conditional debug logging controlled by `DEBUG = Prefs['debug']` (lines 7-11, 156)
- Log before API requests (lines 23-24, 45-47)
- Log matched scenes and scores (line 179)
- Log metadata field operations when debug enabled (lines 195-203, 344, 343, 382, 397, 425, 431, 442, 516, 520)
- Full URL logging on retry (line 36, 66-67)

**Validation:**
- Pre-flight checks before scraping (lines 194-213): RequireOrganized, RequireURL, RequireStashID
- Null checks before accessing nested objects (examples: lines 254, 357, 372, 475, 501)
- Empty string/None checks for titles and dates (lines 87-88, 165, 233, 247)

**Authentication:**
- API key appended to requests from preference `Prefs['APIKey']` (lines 26-27, 48-49, 461-462, 478, 495)
- HTTPS/HTTP selection from preference `Prefs['UseHTTPS']` (lines 29-32, 59-62, 497-500)
- Hostname and port from preferences `Prefs['Hostname']`, `Prefs['Port']` (lines 34, 64, 511)
- Optional Plex token from environment variable for rating sync (line 274)

---

*Architecture analysis: 2026-01-24*
Loading