Ctrl K
ComunidadGuías…

adr/interfaces/adr-006-media-manager.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Media Manager Interface Design with Quality Profile Integration

## Context and Problem Statement

Request management tools like Overseerr need to connect to media managers (Radarr, Sonarr) to handle user requests and automatically add content. The media-manager interface enables this integration by publishing connection details and available quality profiles.

**Key challenges:**
- How should quality profiles be passed from Radarr to Overseerr?
- How should default servers be set when multiple Radarr instances exist (e.g., radarr-1080p, radarr-1080p-remux, radarr-4k)?
- How can we respect user customizations made via Overseerr UI while still providing batteries-included defaults?

**Note:** For how quality profiles are created, see [Apps ADR 003](../apps/adr-003-recyclarr-integration.md).

## Considered Options

### Quality Profile Passing
* **Option A:** Pass profiles via relation data - Radarr publishes profiles, Overseerr receives them without API calls
* **Option B:** Overseerr queries Radarr API - Minimal relation data, Overseerr fetches profiles after connection

### Default Server Selection
* **Option 1:** Query Overseerr API before configuration - Check existing defaults, only set if none exists
* **Option 2:** Peer relation state tracking - Store default assignments in Juju peer relation
* **Option 3:** Alphabetical sorting - Deterministic but arbitrary

### Default Profile Selection  
* **Option 1:** Query-first, set-if-empty - Check if profile already set, use existing or pick first
* **Option 2:** Always use first profile - Overwrite any existing configuration
* **Option 3:** User-specified config - Add charm config for profile selection

## Decision Outcome

**Quality profiles:** Option A - Pass via relation data  
**Default server:** Option 1 - Query Overseerr API  
**Default profile:** Option 1 - Query-first, set-if-empty

### Rationale

**Quality profiles in relation data** because:
- Relation data is the contract - Overseerr shouldn't need extra API calls
- When Recyclarr updates profiles, relation-changed event triggers automatic sync
- All necessary data available immediately for Overseerr configuration

**Query Overseerr API for defaults** because:
- Simplest implementation (~30 lines vs ~50 for peer relations)
- Overseerr's database is source of truth - no hidden state in Juju
- Fully idempotent - repeated reconciles don't change user settings
- No peer relation needed

**Query-first for profiles** because:
- Batteries included with sensible defaults (first profile)
- Respects user customization via Overseerr UI
- Simple logic - no profile name parsing needed

### Consequences

* Good: Complete automation - profiles auto-sync from Recyclarr to Overseerr
* Good: User can customize defaults via familiar Overseerr UI without charm interference
* Good: Idempotent reconciliation - safe to run repeatedly
* Good: First-related instance becomes default automatically (predictable)
* Bad: Requires API calls on every reconcile (negligible overhead)
* Bad: First profile is arbitrary (but predictable and can be changed in UI)

## Interface Data Models

### Enums

```python
from charmarr_lib.models import MediaManager, RequestManager

# Note: MediaManager and RequestManager enums are defined in charmarr-lib
# See lib/adr-001-shared-arr-code.md for the consolidated enum definitions
```

### Provider Data Model (Updated with Quality Profiles)

```python
from pydantic import BaseModel, Field
from typing import Optional

class QualityProfile(BaseModel):
    """Quality profile from Radarr/Sonarr"""
    id: int
    name: str  # e.g., "HD-Bluray+WEB", "UHD-Bluray+WEB"

class MediaManagerProviderData(BaseModel):
    """Data published by media manager charms (Radarr, Sonarr, etc.)"""

    # Connection details
    api_url: str = Field(description="Full API URL (e.g., http://radarr:7878)")
    api_key_secret_id: str = Field(description="Juju secret ID containing API key")

    # Identity
    manager: MediaManager = Field(description="Type of media manager")
    instance_name: str = Field(description="Juju app name (e.g., radarr-4k)")
    base_path: Optional[str] = Field(default=None, description="URL base path if configured")

    # Configuration (populated from Radarr API after Recyclarr sync)
    quality_profiles: list[QualityProfile] = Field(description="Available quality profiles")
    root_folders: list[str] = Field(description="Available root folder paths")
    variant: ContentVariant = Field(
        default=ContentVariant.STANDARD,
        description="Content variant: standard (catch-all), uhd (4K), or anime"
    )
```

### Requirer Data Model (Unchanged)

```python
class MediaManagerRequirerData(BaseModel):
    """Data published by request manager charms (Overseerr, Jellyseerr)"""
    
    requester: RequestManager = Field(description="Type of request manager")
    instance_name: str = Field(description="Juju app name")
```

## Implementation Details

### Radarr Charm: Query and Publish Quality Profiles

```python
class RadarrCharm(CharmBase):
    def _reconcile(self, event):
        """Main reconciler - handles all events."""
        # ... other reconciliation logic ...
        
        # After profiles are created (see Apps ADR 003),
        # query and publish them to the media-manager relation
        self._publish_quality_profiles()
    
    def _publish_quality_profiles(self):
        """Query Radarr API and publish profiles to relation."""
        # Query Radarr for current profiles
        profiles_response = requests.get(
            f"{self.radarr_url}/api/v3/qualityprofile",
            headers={"X-Api-Key": self.api_key}
        )
        profiles = [
            QualityProfile(id=p["id"], name=p["name"])
            for p in profiles_response.json()
        ]
        
        # Query root folders
        folders_response = requests.get(
            f"{self.radarr_url}/api/v3/rootfolder",
            headers={"X-Api-Key": self.api_key}
        )
        root_folders = [f["path"] for f in folders_response.json()]

        # Get variant from charm config
        variant = ContentVariant(self.config.get("variant", "standard"))

        # Publish to relation
        provider_data = MediaManagerProviderData(
            api_url=f"http://{self.app.name}:7878",
            api_key_secret_id=self.api_key_secret_id,
            manager=MediaManager.radarr,
            instance_name=self.app.name,
            quality_profiles=profiles,
            root_folders=root_folders,
            variant=variant,
        )
        self.media_manager_provider.publish_data(provider_data)
```

### Overseerr Charm: Query-First Configuration

```python
class OverseerrCharm(CharmBase):
    def _configure_radarr_server(self, provider: MediaManagerProviderData):
        """Configure or update Radarr server in Overseerr."""
        
        # 1. Query existing Radarr servers from Overseerr
        response = requests.get(
            f"{self.overseerr_url}/api/v1/settings/radarr",
            headers={"X-Api-Key": self.overseerr_api_key}
        )
        existing_servers = response.json()
        
        # 2. Check if this server already exists
        existing_server = next(
            (s for s in existing_servers if s['name'] == provider.instance_name),
            None
        )
        
        # 3. Determine default server flag (only if no default exists for this tier)
        # 4K tier: variant == ContentVariant.UHD
        is_4k_tier = provider.variant == ContentVariant.UHD
        has_default_for_tier = any(
            s.get('isDefault', False) and
            s.get('is4k', False) == is_4k_tier
            for s in existing_servers
            if s['name'] != provider.instance_name  # Exclude current server
        )
        is_default = not has_default_for_tier
        
        # 4. Determine quality profile (use existing or pick first)
        if existing_server and existing_server.get('activeProfileId'):
            # User has set a profile - respect it
            profile_id = existing_server['activeProfileId']
        else:
            # No profile set - use first available from Radarr
            profile_id = provider.quality_profiles[0].id
        
        # 5. Configure server in Overseerr
        server_config = {
            "name": provider.instance_name,
            "hostname": provider.api_url.split("://")[1].split(":")[0],
            "port": int(provider.api_url.split(":")[-1]),
            "apiKey": self._get_api_key(provider.api_key_secret_id),
            "useSsl": provider.api_url.startswith("https"),
            "is4k": is_4k_tier,  # UHD variant maps to Overseerr's 4K tier
            "isDefault": is_default,
            "activeProfileId": profile_id,
            "activeDirectory": provider.root_folders[0],
        }
        
        if existing_server:
            # Update existing
            requests.put(
                f"{self.overseerr_url}/api/v1/settings/radarr/{existing_server['id']}",
                headers={"X-Api-Key": self.overseerr_api_key},
                json=server_config
            )
        else:
            # Create new
            requests.post(
                f"{self.overseerr_url}/api/v1/settings/radarr",
                headers={"X-Api-Key": self.overseerr_api_key},
                json=server_config
            )
```

## User Experience Flow

```bash
# Deploy Radarr instances with variant config (see Apps ADR 004):
juju deploy radarr radarr --config variant="standard"
juju deploy radarr radarr-4k --config variant="4k"
juju deploy radarr radarr-anime --config variant="anime"

# Relate to Overseerr:
juju relate overseerr radarr
juju relate overseerr radarr-4k
juju relate overseerr radarr-anime

# Result in Overseerr (automatic):
# - radarr: isDefault=true, is4k=false, profile="HD-Bluray+WEB"
# - radarr-4k: isDefault=true, is4k=true, profile="UHD-Bluray+WEB"
# - radarr-anime: isDefault=false, is4k=false, profile="Anime"

# User can customize in Overseerr UI (optional):
# - Change default server for standard or 4K tier
# - Change default profile per server
# - Charm respects UI changes on next reconcile ✅
```

## Overseerr API Reference

**Get Radarr servers:**
```http
GET /api/v1/settings/radarr
X-Api-Key: {overseerr_api_key}

Response: [
  {
    "id": 1,
    "name": "radarr-1080p",
    "is4k": false,
    "isDefault": true,
    "activeProfileId": 1
  }
]
```

**Add/Update Radarr server:**
```http
POST /api/v1/settings/radarr
PUT /api/v1/settings/radarr/{id}
X-Api-Key: {overseerr_api_key}

Body: {
  "name": "radarr-1080p",
  "hostname": "radarr-1080p",
  "port": 7878,
  "apiKey": "...",
  "is4k": false,
  "isDefault": true,
  "activeProfileId": 1,
  "activeDirectory": "/data/media/movies"
}
```

## Related Decisions

- [Apps ADR 003](../apps/adr-003-recyclarr-integration.md) - How quality profiles are created in Radarr using Trash Guides
- [lib ADR-003](../lib/adr-003-reconciliation-philosophy.md) - Reconciliation philosophy (respectful reconciliation for quality profiles)
- **Media-indexer interface** - Similar passive provider pattern for Prowlarr ↔ arr apps
- **Download-client interface** - Similar passive provider pattern for download clients ↔ arr apps