commit 9c4a8d96bcc83e3ebcd29e2f4b58efbb8e10e487 Author: Cliff Hill Date: Sat Oct 18 09:14:10 2025 -0400 Initial commit. Signed-off-by: Cliff Hill diff --git a/.env b/.env new file mode 100644 index 0000000..2be73a9 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +# Database Configuration +POSTGRES_DB=plex_playlist +POSTGRES_USER=plex_user +DATABASE_NAME=plex_playlist +DATABASE_HOST=database +DATABASE_PORT=5432 + +# Application Configuration +ENVIRONMENT=production +BACKEND_PORT=8000 +FRONTEND_PORT=80 + +# Development Overrides +DEV_DATABASE_PORT=5433 +DEV_BACKEND_PORT=8001 +DEV_FRONTEND_PORT=5173 + +# Node Environment +NODE_ENV=production diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4cd6ee1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,67 @@ +repos: + # General hooks for all files + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-added-large-files + - id: check-yaml + - id: check-json + - id: check-toml + - id: mixed-line-ending + + # TOML linting + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.14.0 + hooks: + - id: pretty-format-toml + args: [--autofix] + + # Python backend linting and formatting with ruff + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + # Linter + - id: ruff + args: [--fix] + files: ^backend/ + # Formatter + - id: ruff-format + files: ^backend/ + + # Python type checking with pyright + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.384 + hooks: + - id: pyright + files: ^backend/ + additional_dependencies: [fastapi, uvicorn, sqlalchemy, pydantic] + + # Frontend linting and formatting + - repo: local + hooks: + # ESLint + - id: eslint + name: eslint + entry: bash -c 'cd frontend && npm run lint:fix' + language: system + files: ^frontend/.*\.(js|ts|vue)$ + pass_filenames: false + + # Prettier + - id: prettier + name: prettier + entry: bash -c 'cd frontend && npm run format' + language: system + files: ^frontend/.*\.(js|ts|vue|json|css|scss|md)$ + pass_filenames: false + + # TypeScript type checking + - id: typescript-check + name: typescript-check + entry: bash -c 'cd frontend && npm run type-check' + language: system + files: ^frontend/.*\.(ts|vue)$ + pass_filenames: false diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..b3f5630 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,33 @@ +# Backend Dockerfile for FastAPI with Python 3.13 +FROM python:3.13-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + +# Create and activate virtual environment +ENV VIRTUAL_ENV=/app/.venv +RUN uv venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +# Copy dependency files first for better caching +COPY backend/pyproject.toml backend/uv.lock* ./ + +# Install dependencies +RUN uv sync --frozen + +# Copy application code +COPY backend/ . + +# Expose port +EXPOSE 8000 + +# Default command - can be overridden in compose for development +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..6e128aa --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,39 @@ +# Frontend Dockerfile for Vue/Vite TypeScript project +FROM node:20-alpine AS base + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY frontend/package*.json ./ +COPY frontend/yarn.lock* frontend/pnpm-lock.yaml* ./ + +# Install dependencies +RUN if [ -f yarn.lock ]; then yarn install; \ + elif [ -f pnpm-lock.yaml ]; then npm install -g pnpm && pnpm install; \ + else npm install; fi + +# Copy application code +COPY frontend/ . + +# Development stage +FROM base AS development +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] + +# Production build stage +FROM base AS build +RUN if [ -f yarn.lock ]; then yarn build; \ + elif [ -f pnpm-lock.yaml ]; then pnpm build; \ + else npm run build; fi + +# Production stage +FROM nginx:alpine AS production +# Copy built assets from build stage +COPY --from=build /app/dist /usr/share/nginx/html +# Copy nginx configuration +COPY frontend/nginx.conf /etc/nginx/nginx.conf +# Expose port +EXPOSE 80 +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e79293 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Plex Playlist Project + +A full-stack application for managing Plex playlists with a FastAPI backend and Vue.js frontend. + +## Architecture + +- **Backend**: Python 3.13 + FastAPI + uv + ruff +- **Frontend**: TypeScript + Vue.js + Vite +- **Database**: PostgreSQL 16 +- **Containerization**: Docker + Docker Compose + +## Development Setup + +### Prerequisites + +- Docker and Docker Compose +- Git + +### Running in Development Mode + +1. Clone the repository and navigate to the project directory: + ```bash + cd plex-playlist + ``` + +2. Start the development environment: + ```bash + docker compose -f compose.yml -f compose.dev.yml up --build + ``` + +This will start: +- PostgreSQL database on port 5433 +- FastAPI backend on port 8001 (with hot reload) +- Vue.js frontend on port 5173 (with hot reload) + +### Running in Production Mode + +```bash +docker compose up --build -d +``` + +This will start: +- PostgreSQL database on port 5432 +- FastAPI backend on port 8000 +- Vue.js frontend on port 80 + +## Project Structure + +``` +plex-playlist/ +├── backend/ # FastAPI backend +├── frontend/ # Vue.js frontend +│ └── nginx.conf # Nginx configuration +├── Dockerfile.backend # Backend Docker image +├── Dockerfile.frontend # Frontend Docker image +├── compose.yml # Production Docker Compose +├── compose.dev.yml # Development Docker Compose override +└── README.md +``` + +## Environment Variables + +### Backend +- `DATABASE_URL`: PostgreSQL connection string +- `ENVIRONMENT`: `development` or `production` +- `RELOAD`: Enable uvicorn auto-reload (development only) + +### Frontend +- `NODE_ENV`: `development` or `production` + +## Database + +The PostgreSQL database is configured with: +- Database: `plex_playlist` +- User: `plex_user` +- Password: `plex_password` + +## Development Workflow + +1. Make changes to your code +2. The development containers will automatically reload: + - Backend: uvicorn with `--reload` flag + - Frontend: Vite dev server with hot module replacement + +## API Documentation + +When running, the FastAPI automatic documentation is available at: +- Development: http://localhost:8001/docs +- Production: http://localhost:8000/docs + +--- + +# Manual Setup (if not using Docker) + +## Backend Setup (FastAPI, Python 3.13, uv, ruff, pyright) + +### 1. Create and activate the uv virtual environment + +```sh +uv venv plex-playlist +source plex-playlist/bin/activate +``` + +### 2. Install dependencies + +```sh +uv pip install fastapi uvicorn[standard] +uv pip install ruff +``` + +### 3. Install dev tools + +```sh +uv pip install pyright +``` + +### 4. Project structure + +- `app/` - FastAPI application code +- `tests/` - Test suite +- `pyrightconfig.json` - Pyright type checking config +- `ruff.toml` - Ruff linter config + +### 5. Run the development server + +```sh +uvicorn app.main:app --reload +``` + +--- + +## Frontend Setup (Vue 3, Vite, TypeScript) + +### 1. Create the project + +```sh +cd frontend +npm create vite@latest . -- --template vue-ts +npm install +``` + +### 2. Recommended: Enable strictest TypeScript settings + +Edit `tsconfig.json` and set: +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + } +} +``` + +## 3. Run the frontend + +```sh +npm run dev +``` + +--- + +See the `backend/` and `frontend/` folders for more details. diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..3b0e879 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,78 @@ +[tool.ruff] +# Exclude common directories +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv" +] +# Set the maximum line length +line-length = 88 +target-version = "py313" + +[tool.ruff.format] +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" +# Like Black, use double quotes for strings. +quote-style = "double" +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +[tool.ruff.lint] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +# Ignore specific rules +ignore = [ + "E501", # line too long (handled by line-length setting) + "B008", # do not perform function calls in argument defaults + "RUF012" # mutable class attributes should be annotated with typing.ClassVar +] +# Enable specific rule sets +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "PTH", # flake8-use-pathlib + "RUF" # ruff-specific rules +] +unfixable = [] + +[tool.ruff.lint.isort] +known-first-party = ["app"] + +[tool.ruff.lint.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = ["PLR2004", "S101", "TID252"] diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json new file mode 100644 index 0000000..154309b --- /dev/null +++ b/backend/pyrightconfig.json @@ -0,0 +1,34 @@ +{ + "reportMissingImports": true, + "reportMissingTypeStubs": false, + "pythonVersion": "3.13", + "pythonPlatform": "Linux", + "executionEnvironments": [ + { + "root": "." + } + ], + "typeCheckingMode": "strict", + "useLibraryCodeForTypes": true, + "reportGeneralTypeIssues": true, + "reportPropertyTypeMismatch": true, + "reportFunctionMemberAccess": true, + "reportMissingParameterType": true, + "reportMissingTypeArgument": true, + "reportIncompatibleMethodOverride": "error", + "reportIncompatibleVariableOverride": true, + "reportInconsistentConstructor": true, + "strictListInference": true, + "strictDictionaryInference": true, + "strictSetInference": true, + "reportCallInDefaultInitializer": true, + "reportUnnecessaryIsInstance": true, + "reportUnnecessaryCast": true, + "reportUnnecessaryComparison": true, + "reportUnnecessaryContains": true, + "reportImplicitStringConcatenation": true, + "reportUnusedCoroutine": true, + "reportPrivateUsage": "warning", + "reportConstantRedefinition": "warning", + "reportImplicitOverride": "warning" +} diff --git a/compose.dev.yml b/compose.dev.yml new file mode 100644 index 0000000..fc3c090 --- /dev/null +++ b/compose.dev.yml @@ -0,0 +1,37 @@ +services: + database: + ports: + - "5433:5432" # Different port to avoid conflicts with local PostgreSQL + + backend: + build: + context: . + dockerfile: Dockerfile.backend + environment: + DATABASE_URL: postgresql://plex_user:plex_password@database:5432/plex_playlist + ENVIRONMENT: development + RELOAD: "true" + volumes: + - ./backend:/app + - backend_venv:/app/.venv + ports: + - "8001:8000" # Different port for development + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + target: development + volumes: + - ./frontend:/app + - frontend_node_modules:/app/node_modules + ports: + - "5173:5173" # Vite default dev port + environment: + - NODE_ENV=development + +volumes: + postgres_data: + backend_venv: + frontend_node_modules: diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..36a0fca --- /dev/null +++ b/compose.yml @@ -0,0 +1,44 @@ +services: + database: + image: postgres:16-alpine + environment: + POSTGRES_DB: plex_playlist + POSTGRES_USER: plex_user + POSTGRES_PASSWORD: plex_password + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U plex_user -d plex_playlist"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: . + dockerfile: Dockerfile.backend + environment: + DATABASE_URL: postgresql://plex_user:plex_password@database:5432/plex_playlist + ENVIRONMENT: production + ports: + - "8000:8000" + depends_on: + database: + condition: service_healthy + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + target: production + ports: + - "80:80" + depends_on: + - backend + restart: unless-stopped + +volumes: + postgres_data: diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..0f77490 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,38 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Handle client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Enable gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..aa2f144 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "extends": ["config:recommended"], + "packageRules": [ + { + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], + "automerge": true + } + ], + "osvVulnerabilityAlerts": true +} diff --git a/secrets/postgres_password b/secrets/postgres_password new file mode 100644 index 0000000..82584c6 --- /dev/null +++ b/secrets/postgres_password @@ -0,0 +1 @@ +plex_password