Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,7 @@ GitHub.sublime-settings
!.vscode/launch.json
!.vscode/extensions.json
.history

# ai
.claude
CLAUDE.local.md
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/poc.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 100 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Oscarr is a Discord bot that integrates with Plex and Ombi to provide a Discord interface for managing media requests. The project consists of a Django backend that serves both as an admin interface and the bot's data layer.

## Development Commands

All commands should be run from `/packages/django-app/` directory using the `just` task runner:

### Setup & Initialization
- `just init-dcp` - Initialize Docker volumes and build images
- `just dcp-generate-secret-key` - Generate Django secret key for .env
- `just generate-pg-secret-key` - Generate PostgreSQL encryption key
- `just create-superuser` - Create Django admin user
- `just dcp-load-dev-data` - Load fixture data (admin:password login)
- `just setup-local-python-venv` - Create local Python virtual environment and install dependencies

### Development Workflow
- `just dcp-up-all` - Start all containers (web + db)
- `just dcp-migrate` - Run database migrations
- `just start-discord-bot` - Launch Discord bot in container
- `just sync-with-plex` - Sync movie data from Plex server

### Testing & Code Quality
- `just dcp-run-tests` - Run pytest test suite (excludes integration tests)
- `just dcp-format` - Format Python code with autopep8

### Backup & Data Management
- `just dcp-dumpdata` - Export database to JSON
- `just create-json-backup` - Create timestamped JSON backup
- `just create-pgdump` - Create PostgreSQL dump backup

### Utility Commands
- `just update-oscarr` - Pull latest code, build, migrate, and restart containers
- `just dcp-cleanup` - Stop and remove all containers and volumes
- `just dcp-build-images` - Build both web and database Docker images

## Architecture

### Django Apps Structure
- `core/` - User management with custom User model, Discord/Ombi integration
- `plex/` - Plex server data models and sync functionality
- `movie_requests/` - Movie request handling commands
- `watchbot/` - Additional bot functionality
- `discordbot/` - Discord bot implementation with commands
- `services/` - External API integrations (Ombi, Radarr, TMDB)

### Key Patterns
- **Repository Pattern**: Base repository classes in `common/repositories/`
- **Management Commands**: Custom Django commands for bot operations and data sync
- **Signal Integration**: Django signals for automated workflows (e.g., Ombi user creation)
- **Encrypted Fields**: Uses django-pgcrypto-fields for sensitive data

### Discord Bot Commands
- `/search_tmdb` - Search TMDB for movies with request buttons
- `/request <tmdb_id>` - Request movie by TMDB ID
- `/search_plex` - Search Plex library (by title, actor, director, producer)
- `/get_random` - Get random movie from Plex
- `/bacon from: <actor1> to: <actor2>` - Show actor connections through movies

## Environment Setup

The project requires a `.env` file in `/packages/django-app/` with configuration for:
- Django secret keys
- PostgreSQL credentials
- Plex server connection
- Ombi API credentials
- Discord bot token

See `.env.template` for required environment variables.

## Technology Stack

- **Backend**: Django 4.2.5, PostgreSQL 15.4
- **Bot**: discord.py
- **Media APIs**: PlexAPI, TMDB Simple
- **External Services**: Ombi, Radarr
- **Testing**: pytest with Django integration
- **Containerization**: Docker Compose
- **Task Runner**: Just (justfile)
- **Code Quality**: autopep8, pylint, flake8

## Development Notes

- Uses Python 3.11.4 with Pipenv for dependency management
- PostgreSQL with pgcrypto extension for encrypted fields
- All Django management commands should be run via `./bin/dcp-django-admin.sh` for Docker
- Tests exclude integration tests by default (use `-m "not integration"`)
- The bot runs as a separate management command (`start_discord_bot`)
- Plex sync should be run periodically to keep movie data current

## Local Development Setup

1. **Python Environment**: Use pyenv to install Python 3.11.4, then run `just setup-local-python-venv`
2. **Docker Setup**: Run `just init-dcp` to initialize Docker containers and volumes
3. **Database**: Run `just dcp-migrate` to apply database migrations
4. **Development Data**: Run `just dcp-load-dev-data` to load test data (admin:password)
6 changes: 6 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# TODOs 5/23/2025

- [ ] create ombi user on user create
- [ ] add bot command that only admin can use to create user
- [ ] would be extra nice to be able to select discord user from discord
interface to get discord user id for the new user when sending command
4 changes: 4 additions & 0 deletions packages/django-app/app/core/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'

def ready(self):
"""Import signal handlers when the app is ready."""
import core.signals
35 changes: 35 additions & 0 deletions packages/django-app/app/core/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging
from django.db.models.signals import post_save
from django.dispatch import receiver

from core.models import User
from services.ombi import Ombi

logger = logging.getLogger(__name__)


@receiver(post_save, sender=User)
def create_ombi_user(sender, instance, created, **kwargs):
"""
Signal handler to create an Ombi user when a Django user is created,
and set the ombi_uid field on the Django user.
"""
if created and not instance.ombi_uid:
try:
logger.info(f"Creating Ombi user for Django user {instance.nickname}")

# Create user in Ombi
response = Ombi.create_user(username=instance.nickname)

# Extract user ID from response and update Django user
if response and 'id' in response:
ombi_uid = response['id']
logger.info(f"Updating Django user {instance.nickname} with Ombi UID {ombi_uid}")

# Update the user without triggering the signal again
User.objects.filter(id=instance.id).update(ombi_uid=ombi_uid)
else:
logger.error(f"Failed to get Ombi UID from response: {response}")

except Exception as e:
logger.error(f"Error creating Ombi user for {instance.nickname}: {str(e)}")
59 changes: 59 additions & 0 deletions packages/django-app/app/services/ombi.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,62 @@ def create_request(cls, data: dict) -> dict:

data = res.json()
return data

@classmethod
def create_user(cls, username: str, email: str = None, password: str = None) -> dict:
"""
Creates a user in Ombi and returns the created user data including the user ID.

Args:
username: The username for the new Ombi user
email: Optional email for the user
password: Optional password for the user

Returns:
dict: The created user data containing the Ombi user ID
"""
endpoint = f'{cls.base_url}/Identity'
logger.info(f"Ombi create user endpoint: {endpoint}")
headers = {
'content-type': 'application/json',
'ApiKey': settings.OMBI_API_KEY,
}

# Create user data
user_data = {
"userName": username,
"userType": 1,
"movieRequestLimit": 0,
"episodeRequestLimit": 0,
"musicRequestLimit": 0,
"streamingCountry": "us",
"movieRequestLimitType": 0,
"musicRequestLimitType": 0,
"episodeRequestLimitType": 0,
"hasLoggedIn": False,
"source": "local"
}

# Add optional fields if provided
if email:
user_data["emailAddress"] = email
if password:
user_data["password"] = password

logger.info(f"Creating Ombi user: {username}")
logger.info(f"Request data: {json.dumps(user_data, indent=2)}")
logger.info(f"Request headers: {headers}")

res = requests.post(
endpoint,
auth=HTTPBasicAuth(settings.SEEDBOX_UN, settings.SEEDBOX_PW),
data=json.dumps(user_data),
headers=headers)

if not res.ok:
logger.error(f"Failed to create Ombi user: {res.status_code} - {res.text}")
raise Exception(f"Failed to create Ombi user: {res.status_code} - {res.text}")

data = res.json()
logger.info(f"Successfully created Ombi user: {data}")
return data