Compare commits

..

No commits in common. "feature-branch" and "main" have entirely different histories.

58 changed files with 2334 additions and 8993 deletions

View file

@ -1,35 +0,0 @@
# ==============================================================================
# LOCAL DEVELOPMENT
# ==============================================================================
# Usage: cp .env.development .env
# ==============================================================================
FLASK_ENV=development
# Azure AD
AZURE_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
REDIRECT_URI=http://localhost:3000
# API
BACKEND_PORT=5000
API_BASE_URL=http://localhost:5000/api
FRONTEND_URL=http://localhost:3000
# File Management
MAX_FILE_SIZE_MB=500
FILE_RETENTION_HOURS=24
CLEANUP_CHECK_INTERVAL_MINUTES=60
# Security
SECRET_KEY=dev-secret-key-for-local-development-only
# Logging
LOG_LEVEL=DEBUG
# Box Integration
BOX_CONFIG_PATH=oliver_box_config.json
BOX_PROCESSOR_PORT=5001
BOX_WEBHOOK_SECRET=
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID=

View file

@ -1,45 +0,0 @@
# ==============================================================================
# VIDEO OPTIMIZER - ENVIRONMENT CONFIGURATION
# ==============================================================================
# Quick Start:
# Development: cp .env.development .env
# Production: cp .env.production .env (update SECRET_KEY!)
#
# Documentation: ENVIRONMENT_SETUP.md
# ==============================================================================
# Environment
FLASK_ENV=development
# Azure AD
AZURE_CLIENT_ID=your-client-id-here
AZURE_TENANT_ID=your-tenant-id-here
REDIRECT_URI=http://localhost:3000
# API
BACKEND_PORT=5000
API_BASE_URL=http://localhost:5000/api
FRONTEND_URL=http://localhost:3000
# File Management
MAX_FILE_SIZE_MB=500
FILE_RETENTION_HOURS=24
CLEANUP_CHECK_INTERVAL_MINUTES=60
# Security (Generate: python3 -c "import secrets; print(secrets.token_hex(32))")
SECRET_KEY=dev-secret-key-change-in-production
# Logging (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOG_LEVEL=INFO
# Box Integration — runs on the same port as the web API (BACKEND_PORT)
# Webhook endpoint: POST http://your-server/webhooks/box
BOX_CONFIG_PATH=oliver_box_config.json
BOX_WEBHOOK_SECRET=your-webhook-secret-from-box
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID=
# Polling mode (alternative to webhooks - no public URL required)
# Set BOX_USE_POLLING=true to check IN folder on an interval instead of waiting for webhooks
BOX_USE_POLLING=false
BOX_POLL_INTERVAL_SECONDS=60

View file

@ -1,37 +0,0 @@
# ==============================================================================
# PRODUCTION
# ==============================================================================
# Deploy to: /opt/video-optimizer-back/.env
#
# IMPORTANT: Generate SECRET_KEY before deployment
# python3 -c "import secrets; print(secrets.token_hex(32))"
# ==============================================================================
FLASK_ENV=production
# Azure AD
AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
REDIRECT_URI=https://ai-sandbox.oliver.solutions/video-optimizer
# API
BACKEND_PORT=5013
API_BASE_URL=https://ai-sandbox.oliver.solutions/video-optimizer/api
FRONTEND_URL=https://ai-sandbox.oliver.solutions/video-optimizer
# File Management
MAX_FILE_SIZE_MB=500
FILE_RETENTION_HOURS=24
# Security - REPLACE WITH GENERATED KEY
SECRET_KEY=CHANGE-THIS-TO-A-RANDOM-SECRET-KEY
# Logging
LOG_LEVEL=WARNING
# Box Integration
BOX_CONFIG_PATH=/opt/video-optimizer-back/oliver_box_config.json
BOX_PROCESSOR_PORT=5001
BOX_WEBHOOK_SECRET=your-webhook-secret-from-box
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID=

13
.gitignore vendored
View file

@ -14,20 +14,14 @@ build/
# Flask
instance/
# Environment variables (active configuration with secrets)
.env
.env.*.temp
# Note: Template files (.env.example, .env.development, .env.production) ARE committed
# Video files in upload/output folders
backend/uploads/*
backend/outputs/*
!backend/uploads/.gitkeep
!backend/outputs/.gitkeep
md_files/
backend/test.ipynb
# Generated platform specifications
backend/platform_specs.json
backend/naming_conventions.json
@ -47,10 +41,6 @@ Thumbs.db
# Logs
*.log
*.pdf
# Claude
.claude/
# Node
node_modules/
@ -67,4 +57,3 @@ target/
# Unit test reports
TEST*.xml
mc_box_config.json

View file

@ -1 +0,0 @@
Access all Denied

340
ADMIN_GUIDE.md Normal file
View file

@ -0,0 +1,340 @@
# Admin Panel Guide
## Overview
The Admin Panel allows you to manage platform specifications, add new platforms, edit existing configurations, and import/export settings.
**Access:** http://localhost:8888/admin.html (or http://localhost:8000/admin.html for standard setup)
---
## Features
### 📊 Metrics Dashboard
View real-time statistics:
- **Total Platforms** - Number of configured platforms
- **Total Configurations** - Total format combinations across all platforms
- **Unique Codecs** - Number of different codecs in use
- **Aspect Ratios** - Total unique aspect ratios supported
### 🎛 Platform Management
**View All Platforms:**
- See all configured platforms with their specifications
- Each platform shows: Name, Key, Codec, Format count
- Detailed table of all aspect ratios with resolutions and bitrates
**Add New Platform:**
1. Click **"+ Add New Platform"**
2. Fill in platform details:
- **Platform Key** (lowercase, no spaces - used in API)
- **Platform Name** (display name)
- **Video Codec** (H264, H265, or VP9)
- **Container Format** (MP4, WebM, MOV)
3. Add format configurations (aspect ratios):
- Aspect Ratio (e.g., 16:9)
- Resolution (e.g., 1920x1080)
- Bitrate (recommended, e.g., 1500k)
- Min/Max Bitrate (range)
- Audio Bitrate (e.g., 128k)
- Audio Codec (optional - defaults to AAC)
- Note (optional)
4. Click **"+ Add Format Configuration"** for multiple aspect ratios
5. Click **"Save Platform"**
**Edit Platform:**
1. Click **"Edit"** button on any platform card
2. Modify settings (platform key cannot be changed)
3. Add/remove format configurations
4. Click **"Save Platform"**
**Delete Platform:**
1. Click **"Delete"** button on any platform card
2. Confirm deletion
3. Platform is removed from system
### 📤 Export/Import
**Export Specifications:**
- Click **"Export Specs (JSON)"**
- Downloads `platform_specs_YYYY-MM-DD.json` file
- Contains all platform configurations
- Use for backups or sharing
**Import Specifications:**
- Click **"Import Specs (JSON)"**
- Select a previously exported JSON file
- Replaces ALL current specifications
- Use for restoring backups or bulk updates
**Reload from Server:**
- Click **"Reload from Server"**
- Refreshes display with current backend data
- Use after manual changes or testing
---
## Example: Adding a New Platform
### Scenario: Add Instagram Reels
**Platform Details:**
- **Key:** `instagram_reels`
- **Name:** `Instagram Reels`
- **Codec:** `libx264` (H264)
- **Container:** `mp4`
**Format Configuration:**
- **Aspect Ratio:** `9:16`
- **Resolution:** `1080x1920`
- **Bitrate:** `2500k`
- **Min Bitrate:** `2000k`
- **Max Bitrate:** `3000k`
- **Audio Bitrate:** `128k`
- **Audio Codec:** `aac`
- **Note:** `Optimized for Instagram Reels`
After saving, the platform will be immediately available in the main app!
---
## Data Persistence
### How Specs Are Saved
When you add, edit, or delete platforms:
1. Changes are applied to **in-memory** PLATFORM_SPECS
2. Automatically saved to **backend/platform_specs.json**
3. File is loaded on server startup
4. Changes persist across server restarts
### File Location
```
backend/platform_specs.json
```
This file is auto-generated and excluded from Git (.gitignore).
### Backup Strategy
**Recommended:**
1. Regularly **export specs** via Admin Panel
2. Save exported JSON files with date stamps
3. Store backups in version control or cloud storage
4. Import when needed to restore
---
## Platform Key Naming
Platform keys are used in:
- API endpoints
- Filename detection
- Internal references
**Rules:**
- Lowercase only
- No spaces (use underscores)
- Unique across all platforms
- Cannot be changed after creation
**Good examples:**
- `tiktok`
- `meta`
- `youtube_ctv`
- `amazon_prime`
**Bad examples:**
- `TikTok` (uppercase)
- `YouTube CTV` (spaces)
- `meta-fb` (hyphens - use underscores)
---
## Format Configuration Details
### Required Fields
- **Aspect Ratio** - Format ratio (e.g., 16:9, 1:1, 9:16)
- **Resolution** - Width x Height (e.g., 1920x1080)
- **Bitrate** - Recommended video bitrate (e.g., 1500k)
- **Min Bitrate** - Minimum acceptable bitrate
- **Max Bitrate** - Maximum acceptable bitrate
- **Audio Bitrate** - Audio bitrate (e.g., 128k)
### Optional Fields
- **Audio Codec** - Override default audio codec (aac, opus, etc.)
- **Note** - Special instructions or warnings
### Bitrate Format
Use FFmpeg bitrate notation:
- `1500k` = 1500 kbps
- `15000k` = 15 Mbps
- `128k` = 128 kbps
---
## Codec Reference
### Video Codecs
| Codec Value | Display Name | Best For | Notes |
|-------------|--------------|----------|-------|
| `libx264` | H264 | Universal compatibility | Most widely supported |
| `libx265` | H265/HEVC | Better compression | TikTok recommended |
| `libvpx-vp9` | VP9 | Quality & efficiency | YouTube preferred |
### Container Formats
| Container | Extensions | Compatible Codecs |
|-----------|-----------|-------------------|
| `mp4` | .mp4 | H264, H265 |
| `webm` | .webm | VP9 |
| `mov` | .mov | H264, H265 |
### Audio Codecs
- `aac` - Default, universal (for MP4)
- `opus` - High quality (for WebM/VP9)
- `mp3` - Legacy support
---
## Validation
The system validates:
- ✅ Duplicate platform keys (prevented)
- ✅ Required fields present
- ✅ Format array not empty
- ✅ Valid bitrate formats
**Not automatically validated:**
- Resolution format (ensure it's `WIDTHxHEIGHT`)
- Aspect ratio accuracy
- Codec compatibility with container
---
## API Endpoints
Admin endpoints require backend access:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/admin/platforms` | POST | Add new platform |
| `/api/admin/platforms/<key>` | PUT | Update platform |
| `/api/admin/platforms/<key>` | DELETE | Delete platform |
| `/api/admin/export` | GET | Export all specs as JSON |
| `/api/admin/import` | POST | Import specs from JSON |
---
## Troubleshooting
### Changes Not Showing in Main App
1. **Click "Reload from Server"** in Admin Panel
2. **Refresh** main app page (hard reload: Cmd+Shift+R)
3. **Check backend logs** for save errors
4. **Verify** platform_specs.json was updated
### Export Not Working
1. Check browser allows downloads
2. Check browser console (F12) for errors
3. Verify backend `/api/admin/export` endpoint works:
```bash
curl http://localhost:5000/api/admin/export
```
### Import Fails
**Check JSON format:**
```json
{
"platform_key": {
"name": "Platform Name",
"codec": "libx264",
"container": "mp4",
"formats": [
{
"ratio": "16:9",
"size": "1920x1080",
"bitrate": "1500k",
"bitrate_min": "1300k",
"bitrate_max": "1700k",
"audio": "128k"
}
]
}
}
```
### Platform Key Can't Be Changed
Platform keys are immutable after creation to prevent breaking:
- Filename detection patterns
- API references
- Existing configurations
**Solution:** Delete and recreate platform with new key
---
## Best Practices
1. **Export regularly** - Create backups before major changes
2. **Test new platforms** - Verify conversions work after adding
3. **Use descriptive keys** - Clear, lowercase identifiers
4. **Document notes** - Add notes for unusual configurations
5. **Validate ranges** - Ensure min ≤ recommended ≤ max bitrates
---
## Security Note
**Current Setup:**
- Admin panel has **NO authentication**
- Suitable for **local development only**
- For production: Add authentication layer
**Production Recommendations:**
- Implement login system
- Use environment variables for credentials
- Restrict admin endpoints to authenticated users
- Add audit logging for changes
---
## Quick Reference
### Adding Platform Checklist
- [ ] Click "Add New Platform"
- [ ] Enter platform key (lowercase, unique)
- [ ] Enter platform name (display)
- [ ] Select codec
- [ ] Select container
- [ ] Add at least one format configuration
- [ ] Verify bitrate ranges
- [ ] Save platform
- [ ] Test in main app
### Editing Platform Checklist
- [ ] Click "Edit" on platform card
- [ ] Modify settings
- [ ] Add/remove format configurations
- [ ] Save changes
- [ ] Reload main app to see changes
---
**Access Admin Panel:** Add `/admin.html` to your frontend URL
**Repository:** https://bitbucket.org/zlalani/loreal-video-optimizer

128
ADMIN_QUICKSTART.md Normal file
View file

@ -0,0 +1,128 @@
# Admin Panel - Quick Start
## 🎛 Access Admin Panel
**MAMP:** http://localhost:8888/admin.html
**Standard:** http://localhost:8000/admin.html
Or click **"Admin Panel →"** link in the main app footer.
---
## 📊 What You Can Do
### View Metrics
- Total platforms configured
- Total format configurations
- Number of codecs in use
- Aspect ratios supported
### Manage Platforms
- **Add** new platforms (TikTok, Meta, YouTube, etc.)
- **Edit** existing platform specifications
- **Delete** platforms you no longer need
- **Export** all specs to JSON (backup)
- **Import** specs from JSON (restore/bulk update)
---
## Add New Platform (Quick)
1. Click **"+ Add New Platform"**
2. Fill in:
- **Platform Key:** `my_platform` (lowercase, no spaces)
- **Platform Name:** `My Platform`
- **Codec:** Select H264, H265, or VP9
- **Container:** Select MP4, WebM, or MOV
3. Add at least one format:
- **Aspect Ratio:** `16:9`
- **Resolution:** `1920x1080`
- **Bitrate:** `1500k`
- **Min Bitrate:** `1300k`
- **Max Bitrate:** `1700k`
- **Audio:** `128k`
4. Click **"Save Platform"**
✅ Platform is now available in main app!
---
## 📝 Example Platforms
### TikTok Configuration
```
Key: tiktok
Name: TikTok
Codec: libx265 (H265)
Container: mp4
Formats:
- 9:16 → 540x960 → 1050k (840-1300k) → Audio: 128k
- 1:1 → 640x640 → 1000k (840-1200k) → Audio: 128k
```
### Meta Configuration
```
Key: meta
Name: Meta (Facebook/Instagram)
Codec: libx264 (H264)
Container: mp4
Formats:
- 1:1 → 720x720 → 1000k (840-1200k) → Audio: 128k
- 16:9 → 1280x720 → 1250k (1100-1400k) → Audio: 128k
```
---
## 💾 Backup & Restore
### Create Backup
1. Click **"Export Specs (JSON)"**
2. File downloads: `platform_specs_2025-10-16.json`
3. Save somewhere safe
### Restore Backup
1. Click **"Import Specs (JSON)"**
2. Select your backup JSON file
3. Confirms: "Specifications imported successfully"
4. All platforms restored!
---
## 🎨 Interface
**Black + Yellow Theme** - Matches main app
**Components:**
- Metrics cards (yellow numbers)
- Platform cards (expandable details)
- Format tables (all aspect ratios)
- Modal form (add/edit platforms)
- Action buttons (yellow primary, outlined secondary)
---
## ⚠️ Important Notes
1. **Changes are immediate** - Saved platforms appear instantly in main app
2. **No authentication** - Admin page is open (add auth for production)
3. **Platform keys immutable** - Can't change key after creation (delete and recreate instead)
4. **Backup before import** - Import replaces ALL existing platforms
5. **Specs persist** - Saved to `backend/platform_specs.json` (auto-loads on restart)
---
## 🔗 Quick Links
- **Main App:** index.html
- **Admin Panel:** admin.html
- **Full Admin Guide:** ADMIN_GUIDE.md
- **API Docs:** README.md
---
**Ready to manage your platforms!** 🎉

410
CLAUDE.md
View file

@ -1,410 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A video optimization tool for social media platforms that converts videos to platform-specific specifications (codec, bitrate, resolution) using FFmpeg. Built for CDMO creative optimization workflows.
**Two operating modes:**
1. **Web UI** — Interactive upload, convert, compare tool with Microsoft SSO authentication
2. **Box Automation** — Unattended pipeline that monitors a Box.com `IN` folder, converts videos based on filename conventions, and delivers results to `OUT_SUCCESS` or `OUT_FAILED`
**Key Capabilities:**
- Microsoft SSO authentication for secure web UI access (frontend-only)
- 21 platform configurations across 8 social media platforms (Meta, Pinterest, Snapchat, TikTok, YouTube, YouTube CTV, Amazon Prime, Amazon Freevee)
- Automatic platform/aspect ratio detection from filename patterns
- Side-by-side video comparison with synchronized playback
- Admin panel for managing platform specifications and naming conventions
- Box.com JWT service account integration with webhook and polling modes
- Full-metadata JSON reports generated per conversion (Phase 3)
- Daily conversion audit logging
## Architecture
**Three-tier architecture (Web UI + Box Automation):**
### Backend — Web API (Python Flask, port 5000)
- **`backend/app.py`** — Main Flask server
- Core: `/api/health`, `/api/config`, `/api/platforms`, `/api/upload`, `/api/convert`, `/api/stream`, `/api/download`, `/api/cleanup`, `/api/detect`
- Admin: `/api/admin/platforms` (POST/PUT/DELETE), `/api/admin/export`, `/api/admin/import`, `/api/admin/reset-factory`, `/api/admin/naming-conventions`
- Factory defaults stored as deep copies at startup for reset functionality
- Persistent storage via JSON files (`platform_specs.json`, `naming_conventions.json`)
- Loads Azure AD config from `.env`, serves via `/api/config`
- **`backend/video_processor.py`** — FFmpeg wrapper
- Uses `ffmpeg-python` library
- `probe_video()` — extracts full metadata (codec, resolution, bitrate, duration)
- `convert_video(platform, aspect_ratio, output_path, custom_bitrate)` — main conversion
- `get_video_info()` — returns formatted metadata dict (used in Box reports)
- Codec params: H264 (preset/crf/profile), H265 (preset/crf), VP9 (deadline/cpu-used/row-mt)
- **`backend/platform_specs.py`** — Platform definitions and detection
- `PLATFORM_SPECS` dict — all 8 platforms with codecs, containers, format arrays
- `FILENAME_PATTERNS` dict — platform detection patterns (case-insensitive)
- `ASPECT_RATIO_PATTERNS` dict — ratio detection patterns
- `detect_platform_from_filename(filename)` → platform key or None
- `detect_aspect_ratio_from_filename(filename)` → ratio string or None
- Dynamically modified by admin panel; persisted to `naming_conventions.json`
- **`backend/conversion_logger.py`** — Audit logging
- Daily JSON files: `backend/logs/conversions/YYYY-MM-DD_conversions.json`
- Logs: timestamp, user_email, platform, aspect_ratio, input/output sizes, reduction %, duration, status
- `user_email='box_automation@system'` for Box-triggered conversions
- **`backend/file_cleanup.py`** — File retention
- Deletes uploads/outputs older than `FILE_RETENTION_HOURS` (default 24h)
- Run via `backend/run_cleanup.py` or cron
### Backend — Box Automation (integrated into app.py, same port 5000)
Box routes and initialisation live directly in `app.py`. `box_processor.py` is a module (no Flask app of its own) — it is imported by `app.py`.
- **`backend/box_client.py`** — Box SDK wrapper
- JWT authentication from `oliver_box_config.json`
- Optional user impersonation via `BOX_AS_USER_ID`
- `authenticate()` — connects as service account or impersonated user
- `discover_folders(folder_id)` — finds IN/OUT_SUCCESS/OUT_FAILED sub-folders
- `download_file()` / `upload_file()` with `download_with_retry()` / `upload_with_retry()` (exponential backoff)
- `list_enterprise_users()` — helper to identify BOX_AS_USER_ID
- **`backend/box_processor.py`** — Box classes module (no Flask app — imported by `app.py`)
- `BoxProcessor` class — core 8-step pipeline:
1. Validate filename (platform + aspect ratio detectable)
2. Download from Box IN folder
3. Convert with `VideoProcessor`
4. Generate JSON report (with original video metadata via `get_video_info()`)
5. Upload optimised video to OUT_SUCCESS
6. Upload JSON report to OUT_SUCCESS
7. **Delete original from IN folder** (`box_client.client.file(file_id).delete()`)
8. Log to conversion audit log + cleanup temp files
- `BoxPoller` class — background polling thread
- Checks IN folder every `BOX_POLL_INTERVAL_SECONDS` (default 60s)
- Activated by `BOX_USE_POLLING=true` in `.env`
- Tracks processed file IDs in memory to prevent double-processing
- Runs as daemon thread alongside Flask
- `verify_box_signature(secret, body, primary, secondary)` — HMAC-SHA256, checks both Box headers
- Files that fail validation → error report uploaded to OUT_FAILED, pipeline exits early
- **`backend/box_setup.py`** — Diagnostic/setup script (run once)
- Validates `oliver_box_config.json`
- Checks `.env` for all Box variables
- Authenticates and lists accessible folders
- Discovers IN/OUT sub-folders
- Prints actionable summary of what is configured vs missing
- **`backend/test_box_processor.py`** — Test harness
- `test_health()` — hits `/health` endpoint
- `test_manual_trigger(file_id, filename)` — end-to-end test with real Box file
### Frontend (Vanilla HTML/JS)
- **`frontend/index.html`** — Main UI (requires Microsoft SSO)
- **`frontend/admin.html`** — Admin panel (requires Microsoft SSO)
- **`frontend/auth.js`** — MSAL Browser v3.5.0 SSO wrapper (sessionStorage tokens)
- **`frontend/app.js`** — Upload, convert, compare, download logic
- **`frontend/admin.js`** — Platform CRUD, export/import, naming conventions
- **`frontend/config.js`** — API_BASE auto-detection (localhost vs production)
- **`frontend/style.css` / `frontend/admin.css`** — Black (#000000) + Yellow (#FFC407) theme, Montserrat
### Deployment
- **`deployment/video-optimizer-backend.service`** — systemd for main Flask API
- **`deployment/box-processor.service`** — systemd for Box automation service
- **`deployment/deploy.sh`** — full production deployment script
- **`deployment/apache-complete.conf`** — Apache vhost config
## Box.com Configuration
### Credentials
- **`oliver_box_config.json`** — Box JWT credentials (gitignored). Contains `clientID`, `clientSecret`, `appAuth` (publicKeyID + encrypted privateKey + passphrase), `enterpriseID: 43984435`
- Service account: `AutomationUser_2499781_HJdLm0ZhaO@boxdevedition.com`
- No `BOX_AS_USER_ID` required — service account has direct access to VIDEO_OPTIMIZER
### Folder IDs (live)
| Folder | ID |
|--------|----|
| VIDEO_OPTIMIZER root | `362124323515` |
| IN | `362125331342` |
| OUT_SUCCESS | `362127206346` |
| OUT_FAILED | `362119054851` |
### Report Structure
Success report (`*_report.json` in OUT_SUCCESS):
```json
{
"status": "success",
"original_file": { "filename", "size_mb", "codec", "resolution", "bitrate", "duration_seconds" },
"optimised_file": { "filename", "size_mb", "size_reduction_percent", "savings_mb" },
"conversion_details": { "platform", "aspect_ratio", "resolution", "codec", "bitrate" }
}
```
Error report (`*_error_report.json` in OUT_FAILED):
```json
{
"status": "error",
"original_file": { "filename" },
"error": { "message", "detected_platform", "detected_aspect_ratio", "reason" }
}
```
## Common Commands
### Development Setup
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r backend/requirements.txt
brew install ffmpeg # macOS
cp .env.example .env # fill in Azure AD + Box credentials
```
### Running the Web UI
```bash
./start.sh # starts both services
# or manually:
cd backend && python app.py # port 5000
cd frontend && python3 -m http.server 3000 # port 3000
```
### Running the Box Service
Box automation starts automatically inside `app.py` — no separate process needed.
```bash
# First time: verify Box configuration
cd backend && python box_setup.py
# Start everything (web UI + Box automation together)
./start.sh # or: python backend/app.py
# Enable polling mode in .env then start normally:
# BOX_USE_POLLING=true
```
### Testing Box Integration
```bash
# Box health check
curl http://localhost:5000/api/box/health
# Manual trigger (replace with real Box file ID)
curl -X POST http://localhost:5000/api/box/trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "BOX_FILE_ID", "filename": "test_tiktok_9x16.mp4"}'
# Test invalid filename (should produce error report in OUT_FAILED)
curl -X POST http://localhost:5000/api/box/trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "BOX_FILE_ID", "filename": "no_platform_or_ratio.mp4"}'
```
### Web API Testing
```bash
curl http://localhost:5000/api/health
curl http://localhost:5000/api/platforms
curl http://localhost:5000/api/admin/export
lsof -ti:5000 | xargs kill # kill if port in use
lsof -ti:5001 | xargs kill
```
### Admin Panel Access
- Dev: http://localhost:3000/admin.html
- Production: https://yourdomain.com/admin.html
## Microsoft SSO Authentication
Frontend-only authentication using MSAL Browser (v3.5.0). Backend endpoints are unauthenticated.
### Auth Flow
1. Frontend fetches Azure AD config from `GET /api/config`
2. MSAL initialises with `clientId`, `tenantId`, `redirectUri`
3. Unauthenticated users see inline login page
4. On login, Microsoft redirects back; MSAL stores tokens in sessionStorage
5. Authenticated users see full application with email + logout in header
### Files
- `.env` — Azure AD credentials
- `backend/app.py` — serves `/api/config`
- `frontend/auth.js` — MSAL wrapper
- `frontend/index.html` / `frontend/admin.html` — inline login pages
### Troubleshooting SSO
- "Unauthorized" → user not in Azure AD tenant
- "Failed to initialize" → check `/api/config` is reachable, check `.env` credentials
- Redirect fails → verify redirect URIs match in Azure Portal app registration
## Key Implementation Details
### Filename Detection (platform_specs.py)
Case-insensitive substring matching in order:
1. Platform: check `FILENAME_PATTERNS[platform]` list for each platform
2. Aspect ratio: check `ASPECT_RATIO_PATTERNS[ratio]` list for each ratio
3. Returns `None` if no match — Box pipeline skips file, UI allows manual selection
**Pattern priority:** `_youtube_ctv_` is checked before `_youtube_` to prevent false matches.
### Video Conversion Flow (Web UI)
1. `POST /api/upload` → UUID storage → probe metadata → detect platform/ratio
2. `POST /api/convert` → lookup format spec → FFmpeg convert → log → return stats
3. `GET /api/stream` → stream for browser playback
4. `DELETE /api/cleanup` → remove files (or auto-cleanup after 24h)
### Video Conversion Flow (Box Automation)
1. File detected in IN (webhook event or poll)
2. Filename validated → download to `/tmp/box_processor/<job_id>/`
3. Probe original with `VideoProcessor.get_video_info()` for report metadata
4. Convert with `VideoProcessor.convert_video()`
5. Generate JSON report (original + optimised metadata)
6. Upload video + report to OUT_SUCCESS
7. Delete original from IN
8. Log to `backend/logs/conversions/`
9. `shutil.rmtree(temp_dir)` cleanup
### Codec Settings
- **H264:** `preset=medium`, `crf=23`, `profile:v=main`, `pix_fmt=yuv420p`
- **H265:** `preset=medium`, `crf=28`, `pix_fmt=yuv420p`, `x265-params=log-level=error`
- **VP9:** `deadline=good`, `cpu-used=2`, `row-mt=1`
- **Audio:** AAC 128k (mobile) / 192k (CTV), Opus 128-192k (VP9)
### Admin Panel State Management
- Changes to `PLATFORM_SPECS`, `FILENAME_PATTERNS`, `ASPECT_RATIO_PATTERNS` applied in-memory
- Auto-persisted to `platform_specs.json` and `naming_conventions.json`
- Factory defaults: deep copies stored at startup; reset deletes JSON files and restores copies
### File Storage
- **Uploads:** `backend/uploads/{uuid}.{ext}`
- **Outputs:** `backend/outputs/{uuid}_optimized.{ext}`
- **Box temp:** `/tmp/box_processor/{job_id}/` (cleaned after each job)
- **Audit logs:** `backend/logs/conversions/YYYY-MM-DD_conversions.json`
- **Persistence:** `backend/platform_specs.json`, `backend/naming_conventions.json` (auto-generated, gitignored)
## Environment Variables
```env
# Azure AD (required for web UI SSO)
AZURE_CLIENT_ID=
AZURE_TENANT_ID=
REDIRECT_URI=http://localhost:3000
# Server
FLASK_ENV=development
BACKEND_PORT=5000
FRONTEND_URL=http://localhost:3000
MAX_FILE_SIZE_MB=500
FILE_RETENTION_HOURS=24
SECRET_KEY=
# Box integration
BOX_CONFIG_PATH=../oliver_box_config.json
BOX_PROCESSOR_PORT=5001
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID= # blank — service account has direct access
BOX_WEBHOOK_SECRET= # only needed for webhook mode
BOX_USE_POLLING=false # true = poll IN folder; false = webhook only
BOX_POLL_INTERVAL_SECONDS=60
```
## Adding New Platforms
### Via Admin Panel (recommended)
1. Navigate to `/admin.html` → Add New Platform
2. Enter key (lowercase, no spaces), name, codec, container
3. Add format entries: ratio, resolution (WxH), bitrate, min/max, audio bitrate
4. Save → persisted to `platform_specs.json`
### Via Code
In `backend/platform_specs.py`:
```python
"platform_key": {
"name": "Display Name",
"codec": "libx264", # libx264 | libx265 | libvpx-vp9
"container": "mp4", # mp4 | webm
"formats": [
{
"ratio": "16:9",
"size": "1920x1080",
"bitrate": "1500k",
"bitrate_min": "1300k",
"bitrate_max": "1700k",
"audio": "128k",
"audio_codec": "aac" # optional
}
]
}
```
Add detection patterns to `FILENAME_PATTERNS`:
```python
'platform_key': ['_pattern1_', '_pattern2_']
```
## Supported Formats
**Input:** MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V (max 500MB)
**Output:** MP4 (H264/H265) or WebM (VP9)
## CORS Configuration
Development: all origins allowed. Production — restrict in `app.py`:
```python
CORS(app, origins=['https://yourdomain.com'])
```
## Production Deployment
```bash
# Via systemd
sudo systemctl start video-optimizer-backend
sudo systemctl start box-processor
sudo systemctl enable video-optimizer-backend
sudo systemctl enable box-processor
# Manual
gunicorn -w 4 -b 0.0.0.0:5000 app:app
python box_processor.py # port 5001
```
## Troubleshooting
### Box authentication fails
```bash
python backend/box_setup.py # full diagnostic
# Verify oliver_box_config.json exists in project root
# Verify BOX_CONFIG_PATH points to it
```
### Box folders not found
- Check service account has been shared on the VIDEO_OPTIMIZER folder in Box
- Run `box_setup.py` — step 4b shows accessible folders
- Verify `BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515`
### File not processed in Box
- Check filename has both platform and aspect ratio patterns
- Check `OUT_FAILED` for `*_error_report.json`
- In polling mode: watch for `[POLLER]` log lines
- In webhook mode: verify webhook URL and `BOX_WEBHOOK_SECRET`
### FFmpeg not found
```bash
ffmpeg -version
brew install ffmpeg
```
### Port conflicts
```bash
lsof -ti:5000 | xargs kill
lsof -ti:5001 | xargs kill
```
### Admin changes not appearing
1. Check backend logs for save errors
2. Verify `backend/platform_specs.json` was updated
3. Hard reload: Cmd+Shift+R
4. Use "Reload from Server" in admin panel
## Design Theme
- **Colors:** Black (`#000000`) background + Yellow (`#FFC407`) accents
- **Font:** Montserrat (Google Fonts)
- **Style:** Dark UI with professional polish

File diff suppressed because it is too large Load diff

View file

@ -1,390 +0,0 @@
# Video Optimizer — Production Deployment
Assumes: server is running, Apache is configured, SSL certificate is active, Azure AD app registration is done.
**Server:** `ai-sandbox.oliver.solutions`
**URL:** `https://ai-sandbox.oliver.solutions/video-optimizer`
**Backend dir:** `/opt/video-optimizer-back/`
**Frontend dir:** `/var/www/html/video-optimizer/`
---
## Step 1 — Transfer Files to Server
Connect via SFTP (FileZilla or scp) and upload the following:
| Local path | Upload to |
|---|---|
| `backend/` (all `.py` files + `requirements.txt`) | `/opt/video-optimizer-back/backend/` |
| `frontend/` (all `.html`, `.js`, `.css` files) | `/opt/video-optimizer-back/frontend/` |
| `deployment/video-optimizer-backend.service` | `/opt/video-optimizer-back/deployment/` |
| `oliver_box_config.json` | `/opt/video-optimizer-back/` |
| `.env.example` | `/opt/video-optimizer-back/` |
**Do NOT upload:**
- `venv/`
- `backend/uploads/`, `backend/outputs/`, `backend/logs/`
- `backend/__pycache__/`, `*.pyc`
- `.git/`
- `backend/platform_specs.json`, `backend/naming_conventions.json` (auto-generated)
**Via scp (alternative to FileZilla):**
```bash
# Run from local machine — create and upload tarball
tar -czf video-optimizer.tar.gz \
--exclude='venv' \
--exclude='backend/uploads' \
--exclude='backend/outputs' \
--exclude='backend/logs' \
--exclude='backend/__pycache__' \
--exclude='backend/*.pyc' \
--exclude='backend/platform_specs.json' \
--exclude='backend/naming_conventions.json' \
--exclude='.git' \
backend/ frontend/ deployment/ oliver_box_config.json .env.example
scp video-optimizer.tar.gz user@ai-sandbox.oliver.solutions:/tmp/
# On server: extract to destination
ssh user@ai-sandbox.oliver.solutions
cd /tmp && tar -xzf video-optimizer.tar.gz
sudo mv backend frontend deployment oliver_box_config.json .env.example /opt/video-optimizer-back/
rm video-optimizer.tar.gz
```
---
## Step 2 — Copy Frontend to Web Root
```bash
sudo cp -r /opt/video-optimizer-back/frontend/* /var/www/html/video-optimizer/
sudo chown -R www-data:www-data /var/www/html/video-optimizer/
```
---
## Step 3 — Configure .env
```bash
cd /opt/video-optimizer-back
sudo cp .env.example .env
sudo nano .env
```
Set these values:
```env
# Environment
FLASK_ENV=production
# Azure AD (already configured)
AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
REDIRECT_URI=https://ai-sandbox.oliver.solutions/video-optimizer
# API
BACKEND_PORT=5000
FRONTEND_URL=https://ai-sandbox.oliver.solutions/video-optimizer
# File management
MAX_FILE_SIZE_MB=500
FILE_RETENTION_HOURS=24
CLEANUP_CHECK_INTERVAL_MINUTES=60
# Security — generate a new key:
# python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=REPLACE_WITH_GENERATED_KEY
# Logging
LOG_LEVEL=WARNING
# Box.com automation
BOX_CONFIG_PATH=../oliver_box_config.json
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID= # leave blank — JWT app scopes handle access directly
BOX_WEBHOOK_SECRET= # set this if using webhook mode (see Box Webhook Setup section below)
BOX_USE_POLLING=true # true = poll IN folder every 60s (simplest, no webhook needed)
BOX_POLL_INTERVAL_SECONDS=60 # set to false + configure webhook for instant processing
```
Secure the credentials files:
```bash
sudo chmod 600 /opt/video-optimizer-back/.env
sudo chown www-data:www-data /opt/video-optimizer-back/.env
sudo chmod 600 /opt/video-optimizer-back/oliver_box_config.json
sudo chown www-data:www-data /opt/video-optimizer-back/oliver_box_config.json
```
---
## Step 4 — Python Virtual Environment & Dependencies
```bash
cd /opt/video-optimizer-back
# Create virtual environment
sudo python3 -m venv venv
# Install dependencies
sudo venv/bin/pip install --upgrade pip
sudo venv/bin/pip install -r backend/requirements.txt
# Create required runtime directories
sudo mkdir -p backend/uploads backend/outputs backend/logs/conversions
# Set all ownership to www-data
sudo chown -R www-data:www-data /opt/video-optimizer-back/
```
---
## Step 5 — Install and Start systemd Service
```bash
# Copy service file to systemd
sudo cp /opt/video-optimizer-back/deployment/video-optimizer-backend.service /etc/systemd/system/
# Reload systemd, enable and start
sudo systemctl daemon-reload
sudo systemctl enable video-optimizer-backend
sudo systemctl start video-optimizer-backend
# Confirm it is running
sudo systemctl status video-optimizer-backend
```
---
## Step 6 — Verify Deployment
```bash
# 1. Service status
sudo systemctl status video-optimizer-backend
# 2. Backend health check (local)
curl http://localhost:5000/api/health
# Expected: {"status":"ok","ffmpeg_installed":true,"timestamp":"..."}
# 3. Backend health check (through Apache/public URL)
curl https://ai-sandbox.oliver.solutions/video-optimizer/api/health
# 4. Box automation status (local)
curl http://localhost:5000/api/box/health
# Expected: {"box_available":true,"box_initialised":true,"folders_configured":true,...}
# 5. Box automation status (through Apache)
curl https://ai-sandbox.oliver.solutions/video-optimizer/api/box/health
# 6. Box full diagnostic (run as www-data to match service account)
sudo -u www-data /opt/video-optimizer-back/venv/bin/python \
/opt/video-optimizer-back/backend/box_setup.py
```
Then open in browser:
- `https://ai-sandbox.oliver.solutions/video-optimizer` — main app (requires SSO login)
- `https://ai-sandbox.oliver.solutions/video-optimizer/admin.html` — admin panel (includes Box history tab)
---
## Step 7 — Updating the Application
When deploying code changes:
```bash
# 1. Upload changed files via SFTP to /opt/video-optimizer-back/
# (overwrite existing files — do not overwrite .env or oliver_box_config.json)
# 2. Sync frontend to web root
sudo cp -r /opt/video-optimizer-back/frontend/* /var/www/html/video-optimizer/
sudo chown -R www-data:www-data /var/www/html/video-optimizer/
# 3. Update Python dependencies (only if requirements.txt changed)
sudo /opt/video-optimizer-back/venv/bin/pip install -r /opt/video-optimizer-back/backend/requirements.txt
# 4. Restart backend service
sudo systemctl restart video-optimizer-backend
sudo systemctl status video-optimizer-backend
# 5. Verify
curl http://localhost:5000/api/health
```
---
## File Cleanup Cron Job
Set up automatic deletion of old uploads/outputs (runs hourly):
```bash
sudo crontab -u www-data -e
# Add this line:
0 * * * * /opt/video-optimizer-back/venv/bin/python /opt/video-optimizer-back/backend/run_cleanup.py >> /opt/video-optimizer-back/backend/logs/cleanup.log 2>&1
```
---
## Apache Config Snippet
This should already be in the existing VirtualHost — included here for reference. Add inside the `<VirtualHost *:443>` block if missing:
```apache
# Frontend — static files
Alias /video-optimizer /var/www/html/video-optimizer
<Directory /var/www/html/video-optimizer>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Backend API — proxy to Flask
<Location /video-optimizer/api>
ProxyPass http://127.0.0.1:5000/api
ProxyPassReverse http://127.0.0.1:5000/api
ProxyTimeout 600
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</Location>
# Box webhook endpoint
<Location /webhooks/box>
ProxyPass http://127.0.0.1:5000/webhooks/box
ProxyPassReverse http://127.0.0.1:5000/webhooks/box
ProxyTimeout 600
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
</Location>
```
After any Apache config change:
```bash
sudo apache2ctl configtest # must say "Syntax OK"
sudo systemctl reload apache2
```
---
## Box Automation — Polling vs Webhook
The application supports two modes for monitoring the Box IN folder:
### Option A — Polling (recommended to start with)
No Box configuration needed. The service checks the IN folder every 60 seconds.
```env
BOX_USE_POLLING=true
BOX_POLL_INTERVAL_SECONDS=60
BOX_WEBHOOK_SECRET= # leave blank
```
Restart the service after changing `.env`:
```bash
sudo systemctl restart video-optimizer-backend
```
Files dropped into Box IN will be processed within 60 seconds automatically.
### Option B — Webhook (instant processing, requires Box Admin setup)
Box calls the server immediately when a file is uploaded — no polling delay.
**Step 1 — Generate a webhook secret:**
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```
**Step 2 — Add to `.env` on the server:**
```env
BOX_USE_POLLING=false
BOX_WEBHOOK_SECRET=<secret-generated-above>
```
**Step 3 — Configure the webhook in Box Admin Console:**
- Go to [app.box.com/master](https://app.box.com/master) → Admin Console → Integrations → Webhooks
- Create a new webhook:
- **URL:** `https://ai-sandbox.oliver.solutions/webhooks/box`
- **Trigger events:** `FILE.UPLOADED`, `FILE.COPIED`
- **Target:** VIDEO_OPTIMIZER IN folder (ID: `362125331342`)
- **Primary key:** paste the secret from Step 1
**Step 4 — Restart:**
```bash
sudo systemctl restart video-optimizer-backend
```
Files uploaded to Box IN will trigger the pipeline within seconds.
---
## Quick Reference
```bash
# Service management
sudo systemctl status video-optimizer-backend # status
sudo systemctl restart video-optimizer-backend # restart
sudo systemctl stop video-optimizer-backend # stop
sudo journalctl -u video-optimizer-backend -f # live logs
sudo journalctl -u video-optimizer-backend -n 50 # last 50 lines
# Health checks
curl http://localhost:5000/api/health
curl http://localhost:5000/api/box/health # Box automation status
# Check port
sudo ss -tulpn | grep 5000
# Disk usage
df -h /opt/video-optimizer-back
sudo du -sh /opt/video-optimizer-back/backend/uploads/
sudo du -sh /opt/video-optimizer-back/backend/outputs/
```
---
## Troubleshooting
**Service won't start:**
```bash
sudo journalctl -u video-optimizer-backend -n 50
# Common causes: missing .env, wrong file ownership, port 5000 in use
sudo ss -tulpn | grep 5000
sudo chown -R www-data:www-data /opt/video-optimizer-back/
```
**502 Bad Gateway from Apache:**
```bash
curl http://localhost:5000/api/health # is backend running?
sudo systemctl restart video-optimizer-backend
sudo tail -f /var/log/apache2/error.log
```
**Missing Python module:**
```bash
sudo /opt/video-optimizer-back/venv/bin/pip install -r /opt/video-optimizer-back/backend/requirements.txt
sudo systemctl restart video-optimizer-backend
```
**Box automation not initialising:**
```bash
sudo -u www-data /opt/video-optimizer-back/venv/bin/python \
/opt/video-optimizer-back/backend/box_setup.py
# Check .env has BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
# Check oliver_box_config.json exists and is readable by www-data
curl http://localhost:5000/api/box/health
```
**Test Box pipeline manually (replace FILE_ID with a real Box file ID from IN folder):**
```bash
curl -X POST http://localhost:5000/api/box/trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "FILE_ID", "filename": "campaign_tiktok_9x16.mp4"}'
```
**Frontend changes not showing:**
```bash
sudo cp -r /opt/video-optimizer-back/frontend/* /var/www/html/video-optimizer/
sudo chown -R www-data:www-data /var/www/html/video-optimizer/
# Hard refresh browser: Cmd+Shift+R / Ctrl+Shift+R
```

View file

@ -1,828 +0,0 @@
# Environment Setup Guide
Complete guide for setting up the Video Optimizer in development and production environments.
---
## 📁 Environment Files
| File | Purpose | Usage |
|------|---------|-------|
| [.env.example](.env.example) | Template with all options and documentation | Reference |
| [.env.development](.env.development) | Local development configuration | Copy to `.env` for development |
| [.env.production](.env.production) | Production server configuration | Deploy to server as `.env` |
**Important:** The `.env` file (your active configuration) is git-ignored and should never be committed.
---
## 🚀 Local Development Setup
### Prerequisites
- Python 3.8 or later
- FFmpeg installed
- Git
### Setup Steps (5 minutes)
```bash
# 1. Clone/navigate to project directory
cd loreal-video-optimizer
# 2. Copy development environment configuration
cp .env.development .env
# 3. (Optional) Edit .env if you need custom settings
nano .env
# 4. Create Python virtual environment
python3 -m venv venv
# 5. Activate virtual environment
source venv/bin/activate # On Windows: venv\Scripts\activate
# 6. Install Python dependencies
pip install -r backend/requirements.txt
# 7. Install FFmpeg (if not already installed)
# macOS:
brew install ffmpeg
# Ubuntu/Debian:
# sudo apt-get install ffmpeg
# 8. Start the application
./start.sh
```
### Access Your Application
- **Frontend:** http://localhost:3000
- **Backend API:** http://localhost:5000/api
- **Admin Panel:** http://localhost:3000/admin.html
- **Help Page:** http://localhost:3000/help.html
### Development Configuration
The [.env.development](.env.development) file is pre-configured with:
```bash
FLASK_ENV=development # Debug mode enabled
BACKEND_PORT=5000 # Local backend port
API_BASE_URL=http://localhost:5000/api
FRONTEND_URL=http://localhost:3000
LOG_LEVEL=DEBUG # Verbose logging
SECRET_KEY=dev-secret-key # Simple key OK for development
```
### Common Development Tasks
**Start the application:**
```bash
./start.sh
```
**Stop the application:**
```
Press Ctrl+C in the terminal
```
**View backend logs:**
```bash
tail -f backend/logs/*.log
```
**Test API health:**
```bash
curl http://localhost:5000/api/health
```
---
## 🌐 Production Deployment
### Prerequisites
- Ubuntu 20.04+ server (or similar Linux distribution)
- SSH access with sudo privileges
- Domain configured: ai-sandbox.oliver.solutions
- Apache web server installed
- SSL certificate configured
### Deployment Steps (15 minutes)
#### Step 1: Prepare Production Environment File
**On your local machine:**
```bash
# 1. Copy production template
cp .env.production .env.prod
# 2. Generate a secure random secret key
python3 -c "import secrets; print(secrets.token_hex(32))"
# This will output something like:
# a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
# 3. Edit the production file
nano .env.prod
# 4. Update these critical values:
# - SECRET_KEY: Paste the generated key
# - Verify AZURE_CLIENT_ID and AZURE_TENANT_ID
# - Verify REDIRECT_URI matches your domain exactly
```
#### Step 2: Verify Azure AD Configuration
1. Log in to [Azure Portal](https://portal.azure.com)
2. Navigate to: **Azure Active Directory → App registrations**
3. Select your production app
4. Under **Authentication**, ensure this redirect URI is registered:
```
https://ai-sandbox.oliver.solutions/video-optimizer
```
5. Save if you made any changes
#### Step 3: Deploy to Server
```bash
# 1. Upload environment file to server
scp .env.prod user@ai-sandbox.oliver.solutions:/tmp/
# 2. Connect to server
ssh user@ai-sandbox.oliver.solutions
# 3. Move to correct location with proper permissions
sudo mv /tmp/.env.prod /opt/video-optimizer-back/.env
sudo chmod 600 /opt/video-optimizer-back/.env
sudo chown www-data:www-data /opt/video-optimizer-back/.env
# 4. Verify file permissions
ls -la /opt/video-optimizer-back/.env
# Should show: -rw------- 1 www-data www-data
```
#### Step 4: Restart Backend Service
```bash
# Restart the backend service
sudo systemctl restart video-optimizer-backend
# Check service status
sudo systemctl status video-optimizer-backend
# View logs
sudo journalctl -u video-optimizer-backend -n 50 -f
```
#### Step 5: Verify Deployment
```bash
# Test API health endpoint
curl https://ai-sandbox.oliver.solutions/video-optimizer/api/health
# Expected response:
# {"status":"ok","ffmpeg_installed":true,"timestamp":"..."}
# Test in browser
open https://ai-sandbox.oliver.solutions/video-optimizer
```
### Production Configuration
The [.env.production](.env.production) file should be configured with:
```bash
FLASK_ENV=production # Production mode (debug disabled)
BACKEND_PORT=5013 # Internal port (accessed via Apache)
API_BASE_URL=https://ai-sandbox.oliver.solutions/video-optimizer/api
FRONTEND_URL=https://ai-sandbox.oliver.solutions/video-optimizer
LOG_LEVEL=WARNING # Minimal logging
SECRET_KEY=<generated-key> # MUST be unique random key
```
### Production Maintenance
**Restart backend service:**
```bash
sudo systemctl restart video-optimizer-backend
```
**View real-time logs:**
```bash
sudo journalctl -u video-optimizer-backend -f
```
**Check service status:**
```bash
sudo systemctl status video-optimizer-backend
```
**Update environment variables:**
```bash
sudo nano /opt/video-optimizer-back/.env
sudo systemctl restart video-optimizer-backend
```
---
## 🔧 Configuration Reference
### Environment Variables
| Variable | Description | Development | Production |
|----------|-------------|-------------|------------|
| `FLASK_ENV` | Flask environment mode | `development` | `production` |
| `BACKEND_PORT` | Backend server port | `5000` | `5013` |
| `API_BASE_URL` | Backend API URL | `http://localhost:5000/api` | `https://domain.com/app/api` |
| `FRONTEND_URL` | Frontend URL (for CORS) | `http://localhost:3000` | `https://domain.com/app` |
| `AZURE_CLIENT_ID` | Azure AD application ID | From Azure Portal | From Azure Portal |
| `AZURE_TENANT_ID` | Azure AD tenant ID | From Azure Portal | From Azure Portal |
| `REDIRECT_URI` | OAuth callback URL | `http://localhost:3000` | `https://domain.com/app` |
| `SECRET_KEY` | Session encryption key | Simple key OK | **Must be random** |
| `MAX_FILE_SIZE_MB` | Max upload size (MB) | `500` | `500` |
| `FILE_RETENTION_HOURS` | Auto-delete after hours | `24` | `24` |
| `LOG_LEVEL` | Logging verbosity | `DEBUG` | `WARNING` |
### Generate Secret Key
```bash
# Use this command to generate a secure secret key
python3 -c "import secrets; print(secrets.token_hex(32))"
```
**Important:** Always generate a new unique key for production. Never reuse keys across environments.
---
## 🔒 Security Best Practices
### Development Environment
- ✅ Simple `SECRET_KEY` is acceptable
- ✅ Can use HTTP (localhost)
- ✅ Debug mode enabled for troubleshooting
- ✅ `.env` file can stay on local machine
### Production Environment
- ⚠️ **MUST generate random `SECRET_KEY`**
- ⚠️ **MUST use HTTPS with valid SSL**
- ⚠️ **NEVER commit `.env` to version control**
- ⚠️ Set file permissions: `chmod 600 .env`
- ⚠️ Set ownership: `chown www-data:www-data .env`
- ⚠️ Store backup of `.env` in secure location
- ⚠️ Rotate `SECRET_KEY` periodically
### File Permissions (Production)
```bash
# Correct permissions for .env file
sudo chmod 600 /opt/video-optimizer-back/.env
sudo chown www-data:www-data /opt/video-optimizer-back/.env
# Verify
ls -la /opt/video-optimizer-back/.env
# Output should be: -rw------- 1 www-data www-data ... .env
```
---
## 🧪 Testing Your Setup
### Test Development
```bash
# 1. Check backend is running
curl http://localhost:5000/api/health
# Expected: {"status":"ok","ffmpeg_installed":true,...}
# 2. Check frontend
open http://localhost:3000
# 3. Test file upload (via browser)
# - Navigate to http://localhost:3000
# - Click "Choose File" and select a video
# - Verify upload and conversion work
```
### Test Production
```bash
# On production server
# 1. Check systemd service
sudo systemctl status video-optimizer-backend
# 2. Check API health
curl https://ai-sandbox.oliver.solutions/video-optimizer/api/health
# 3. Check recent logs
sudo journalctl -u video-optimizer-backend -n 50
# 4. Test in browser
# Navigate to: https://ai-sandbox.oliver.solutions/video-optimizer
# Try uploading and converting a video
# Verify Azure AD login works
```
---
## 🆘 Troubleshooting
### Common Issues
#### ❌ "Module 'dotenv' not found"
```bash
# Solution: Install dependencies
pip install -r backend/requirements.txt
```
#### ❌ "FFmpeg not found"
```bash
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install ffmpeg
# Verify installation
ffmpeg -version
```
#### ❌ "Azure AD authentication failed"
**Check:**
1. `AZURE_CLIENT_ID` and `AZURE_TENANT_ID` are correct
2. `REDIRECT_URI` in `.env` matches Azure Portal **exactly**
3. `REDIRECT_URI` is registered in Azure AD app
4. No trailing slash mismatch (check both)
#### ❌ "CORS policy blocked request"
**Development:**
- CORS should allow all origins automatically
- Check `FLASK_ENV=development` in `.env`
**Production:**
- Check `FRONTEND_URL` matches the actual domain
- Verify no typos in URL
- Check for HTTP vs HTTPS mismatch
#### ❌ "Backend service won't start" (Production)
```bash
# Check logs for errors
sudo journalctl -u video-optimizer-backend -n 100
# Common issues:
# - Port already in use (change BACKEND_PORT)
# - Invalid .env file syntax
# - Missing dependencies
# - FFmpeg not installed
```
#### ❌ "File upload fails / too large"
```bash
# Edit .env file
nano .env # Development
# OR
sudo nano /opt/video-optimizer-back/.env # Production
# Increase limit
MAX_FILE_SIZE_MB=1000
# Restart backend
./start.sh # Development
# OR
sudo systemctl restart video-optimizer-backend # Production
```
---
## 📋 Deployment Checklist
### Pre-Deployment (Production)
- [ ] Generated unique `SECRET_KEY`
- [ ] Updated `SECRET_KEY` in `.env.production`
- [ ] Verified `AZURE_CLIENT_ID` and `AZURE_TENANT_ID`
- [ ] Registered `REDIRECT_URI` in Azure Portal
- [ ] Verified `API_BASE_URL` and `FRONTEND_URL`
- [ ] SSL certificate is valid and configured
- [ ] Apache reverse proxy is configured
- [ ] FFmpeg is installed on server
- [ ] Python 3.8+ is installed on server
### Post-Deployment (Production)
- [ ] Uploaded `.env` to `/opt/video-optimizer-back/`
- [ ] Set file permissions: `chmod 600`
- [ ] Set file ownership: `chown www-data:www-data`
- [ ] Restarted backend service
- [ ] Verified service is running
- [ ] Tested API health endpoint
- [ ] Tested file upload/conversion/download
- [ ] Verified Azure AD login works
- [ ] Checked logs for errors
---
## 🔄 Common Workflows
### Update Environment Variable (Development)
```bash
# 1. Edit .env
nano .env
# 2. Make your changes
# 3. Restart application
./start.sh
```
### Update Environment Variable (Production)
```bash
# 1. Connect to server
ssh user@ai-sandbox.oliver.solutions
# 2. Edit .env
sudo nano /opt/video-optimizer-back/.env
# 3. Save changes
# 4. Restart service
sudo systemctl restart video-optimizer-backend
# 5. Verify
sudo systemctl status video-optimizer-backend
```
### Rotate Secret Key (Production)
```bash
# 1. Generate new key
python3 -c "import secrets; print(secrets.token_hex(32))"
# 2. Update on server
ssh user@ai-sandbox.oliver.solutions
sudo nano /opt/video-optimizer-back/.env
# Replace SECRET_KEY with new value
# 3. Restart service (this will invalidate all active user sessions)
sudo systemctl restart video-optimizer-backend
```
### Switch from Development to Production
```bash
# This should only be done when deploying to a production server
# Never run production mode on localhost
# 1. Prepare production .env as described above
# 2. Deploy entire application to server (see DEPLOYMENT.md)
# 3. Deploy .env file to /opt/video-optimizer-back/.env
# 4. Configure systemd service
# 5. Configure Apache reverse proxy
```
---
## 📦 Box.com Integration (Phase 2 - Automated Workflow)
The Box.com integration provides automated video processing triggered by file uploads to Box folders.
### Architecture
**Services:**
- **Main Web App** (Port 5000) - User-facing Flask application
- **Box Processor** (Port 5001) - Automated Box webhook handler
**Box Folder Structure:**
```
/VIDEO_OPTIMIZER/
├── IN/ (Upload videos here - monitored by webhook)
├── OUT_SUCCESS/ (Optimized videos + JSON reports)
└── OUT_FAILED/ (Error reports for failed conversions)
```
### Setup Requirements
1. **Box.com Configuration:**
- JWT credentials in `oliver_box_config.json`
- Folders inside VIDEO_OPTIMIZER: IN, OUT_SUCCESS, OUT_FAILED
- Webhook configured for IN folder
2. **System Requirements:**
- All standard requirements (Python, FFmpeg, etc.)
- Box SDK installed: `boxsdk==3.9.2`
### Local Development Setup
```bash
# 1. Install Box SDK
source venv/bin/activate
pip install -r backend/requirements.txt
# 2. Place Box config file in project root
# File: oliver_box_config.json (get from Box Admin)
# 3. Update .env with Box variables
nano .env
# Add:
# BOX_CONFIG_PATH=oliver_box_config.json
# BOX_PROCESSOR_PORT=5001
# BOX_WEBHOOK_SECRET=
# BOX_ROOT_FOLDER_ID=0
# 4. Test Box authentication
cd backend
python -c "from box_client import BoxClient; client = BoxClient('../oliver_box_config.json'); client.authenticate(); print(client.discover_folders())"
# 5. Start Box processor service
python box_processor.py
# Service starts on http://localhost:5001
# 6. Test health endpoint
curl http://localhost:5001/health
```
### Production Deployment
**Step 1: Prepare Configuration**
```bash
# 1. Copy Box config to server
scp oliver_box_config.json user@ai-sandbox.oliver.solutions:/tmp/
ssh user@ai-sandbox.oliver.solutions
sudo mv /tmp/oliver_box_config.json /opt/video-optimizer-back/
sudo chmod 600 /opt/video-optimizer-back/oliver_box_config.json
sudo chown www-data:www-data /opt/video-optimizer-back/oliver_box_config.json
# 2. Update production .env with Box variables
sudo nano /opt/video-optimizer-back/.env
# Add:
BOX_CONFIG_PATH=/opt/video-optimizer-back/oliver_box_config.json
BOX_PROCESSOR_PORT=5001
BOX_WEBHOOK_SECRET=your-webhook-secret-from-box-admin
BOX_ROOT_FOLDER_ID=0
```
**Step 2: Install Box SDK**
```bash
# On production server
cd /opt/video-optimizer-back
source venv/bin/activate
pip install boxsdk==3.9.2
```
**Step 3: Deploy Box Processor Service**
```bash
# Copy service file
sudo cp deployment/box-processor.service /etc/systemd/system/
# Reload systemd
sudo systemctl daemon-reload
# Enable and start service
sudo systemctl enable box-processor
sudo systemctl start box-processor
# Check status
sudo systemctl status box-processor
# View logs
sudo journalctl -u box-processor -f
```
**Step 4: Configure Box Webhook**
1. Log in to Box Admin Console
2. Navigate to your application
3. Create Webhook:
- **Target:** `IN` folder (inside VIDEO_OPTIMIZER)
- **Events:** `FILE.UPLOADED`, `FILE.COPIED`
- **URL:** `https://ai-sandbox.oliver.solutions/video-optimizer-box/webhooks/box`
- **Save signature key** to `BOX_WEBHOOK_SECRET` env variable
4. Restart box-processor service
**Step 5: Configure Apache (Optional)**
Add to Apache configuration:
```apache
# Box processor webhook endpoint
ProxyPass /video-optimizer-box http://localhost:5001
ProxyPassReverse /video-optimizer-box http://localhost:5001
```
Restart Apache:
```bash
sudo systemctl restart apache2
```
### Testing Box Integration
**Local Testing (Manual Trigger):**
```bash
# Start box processor
cd backend
python box_processor.py
# In another terminal, test with manual trigger:
curl -X POST http://localhost:5001/manual-trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "BOX_FILE_ID", "filename": "test_tiktok_9x16.mp4"}'
# Or use test script:
python test_box_processor.py
```
**Production Testing (Webhook):**
```bash
# 1. Upload test video to Box IN folder (inside VIDEO_OPTIMIZER)
# Filename must include platform and aspect ratio:
# - valid_tiktok_9x16.mp4 ✓
# - campaign_meta_1x1.mov ✓
# - video_without_platform.mp4 ✗
# 2. Monitor processing
sudo journalctl -u box-processor -f
# 3. Check Box folders
# - OUT_SUCCESS: optimized video + JSON report
# - OUT_FAILED: error report (if invalid filename)
# 4. Check conversion logs
cat /opt/video-optimizer-back/backend/logs/conversions/$(date +%Y-%m-%d)_conversions.json | jq
```
### Filename Convention
**Valid Filenames** (will process):
- `summer_campaign_tiktok_9x16.mp4`
- `product_demo_meta_1x1_final.mov`
- `youtube_16x9_ad.mp4`
- `pinterest_square_promo.mp4`
**Invalid Filenames** (will skip with error report):
- `video_without_platform.mp4` ❌ (missing platform)
- `tiktok_video.mp4` ❌ (missing aspect ratio)
- `generic_video.mp4` ❌ (missing both)
**Platform Patterns:** `_meta_`, `_tiktok_`, `_youtube_`, `_pinterest_`, etc.
**Aspect Ratio Patterns:** `_1x1_`, `_16x9_`, `_9x16_`, `_4x5_`, etc.
See [backend/platform_specs.py](backend/platform_specs.py) for complete list.
### JSON Reports
**Success Report (OUT_SUCCESS):**
```json
{
"status": "success",
"timestamp": "2026-02-13T10:30:45.123456",
"processing_time_seconds": 12.34,
"original_file": {
"filename": "campaign_tiktok_9x16.mp4",
"size_bytes": 25600000,
"size_mb": 24.41
},
"optimized_file": {
"filename": "campaign_tiktok_9x16_optimized.mp4",
"size_bytes": 15360000,
"size_mb": 14.65,
"size_reduction_percent": 40.00,
"savings_mb": 9.77
},
"conversion_details": {
"platform": "tiktok",
"aspect_ratio": "9:16",
"resolution": "540x960",
"codec": "libx265",
"bitrate": "1050k",
"duration_seconds": 30.5
}
}
```
**Error Report (OUT_FAILED):**
```json
{
"status": "error",
"timestamp": "2026-02-13T10:30:45.123456",
"original_file": {
"filename": "video_without_platform.mp4"
},
"error": {
"message": "No platform detected in filename",
"detected_platform": null,
"detected_aspect_ratio": "9:16",
"reason": "Invalid filename format - must include both platform and aspect ratio patterns"
}
}
```
### Monitoring
**Service Status:**
```bash
# Check both services
sudo systemctl status video-optimizer-backend
sudo systemctl status box-processor
# View logs
sudo journalctl -u box-processor -n 100
sudo journalctl -u box-processor -f # Follow live
```
**Conversion Logs:**
```bash
# View today's conversions
cat backend/logs/conversions/$(date +%Y-%m-%d)_conversions.json | jq
# Count conversions
cat backend/logs/conversions/$(date +%Y-%m-%d)_conversions.json | jq 'length'
# Show failed conversions
cat backend/logs/conversions/$(date +%Y-%m-%d)_conversions.json | jq '.[] | select(.status=="failure")'
```
### Troubleshooting Box Integration
**Service won't start:**
```bash
# Check logs for errors
sudo journalctl -u box-processor -n 50
# Common issues:
# - Box config file not found
# - Invalid JWT credentials
# - Required folders not found in Box
# - Port 5001 already in use
```
**Files not processing:**
```bash
# 1. Check webhook delivery in Box Admin Console
# 2. Verify webhook URL is accessible
# 3. Check signature verification (BOX_WEBHOOK_SECRET)
# 4. Test manual trigger:
curl -X POST http://localhost:5001/manual-trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "FILE_ID", "filename": "test_tiktok_9x16.mp4"}'
```
**Authentication failures:**
```bash
# Test Box authentication
cd backend
python -c "
from box_client import BoxClient
client = BoxClient('/opt/video-optimizer-back/oliver_box_config.json')
if client.authenticate():
print('✓ Authentication successful')
folders = client.discover_folders()
print(f'Found folders: {folders}')
else:
print('✗ Authentication failed')
"
```
### Security Notes
1. **JWT Credentials:** Store `oliver_box_config.json` with 600 permissions, owned by www-data
2. **Webhook Signature:** Always verify `BOX_WEBHOOK_SECRET` in production
3. **File Sanitization:** Filenames are sanitized automatically
4. **Temp Cleanup:** Temp files are deleted after processing (even on errors)
---
## 📚 Additional Resources
- **Main Documentation:** [README.md](README.md) - Application overview and features
- **Deployment Guide:** [DEPLOYMENT.md](DEPLOYMENT.md) - Complete server setup instructions
- **Development Guide:** [CLAUDE.md](CLAUDE.md) - Developer documentation
- **Environment Template:** [.env.example](.env.example) - All available variables
---
## 📞 Getting Help
If you encounter issues not covered here:
1. Check the troubleshooting section above
2. Review application logs
3. Verify all configuration values
4. Check Azure AD configuration in Azure Portal
5. Review [DEPLOYMENT.md](DEPLOYMENT.md) for server setup
---
**Version:** 1.0.0
**Last Updated:** 2026-02-12
**Maintained by:** Oliver Team

374
MAMP_SETUP.md Normal file
View file

@ -0,0 +1,374 @@
# MAMP Setup Guide
## Overview
This guide explains how to run the Video Optimizer with **MAMP** hosting the frontend while the Python backend runs separately.
## Architecture
```
MAMP (Port 8888) Python Flask (Port 5000)
↓ ↓
Frontend (HTML/JS) ←→ API Calls → Backend (Video Processing)
```
The frontend runs on MAMP, but makes API calls to the Python backend for video processing.
---
## Step 1: Setup Python Backend
### 1.1 Activate Virtual Environment
```bash
cd /Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction
source venv/bin/activate
```
### 1.2 Start Backend Server
```bash
cd backend
python app.py
```
You should see:
```
Starting Video Optimization Server...
Upload folder: .../backend/uploads
Output folder: .../backend/outputs
* Running on http://0.0.0.0:5000
```
**Keep this terminal running!** The backend must stay active.
---
## Step 2: Configure MAMP
### 2.1 Open MAMP Preferences
1. Open **MAMP** application
2. Go to **Preferences** > **Ports**
3. Verify Apache Port (usually **8888** or **80**)
### 2.2 Set Document Root
1. Go to **Preferences** > **Web Server**
2. Click **"Select"** next to Document Root
3. Navigate to: `/Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/frontend`
4. Click **"Select"**
### 2.3 Start MAMP
1. Click **"Start Servers"**
2. Wait for Apache and MySQL to turn green
---
## Step 3: Configure API Endpoint
### 3.1 Edit config.js
Open `frontend/config.js` and ensure it points to your Python backend:
```javascript
const CONFIG = {
// Python Flask backend URL - MUST be running!
API_BASE: 'http://localhost:5000/api',
MAX_FILE_SIZE: 500 * 1024 * 1024,
DEBUG: true
};
```
### 3.2 Verify Configuration
The default port `5000` should work. If your Python backend is on a different port, update this line.
---
## Step 4: Access the Application
### Open in Browser
Navigate to: **http://localhost:8888/index.html**
Or if MAMP is on port 80: **http://localhost/index.html**
---
## Step 5: Test the Connection
### 5.1 Check Backend Health
Open browser console (F12) and run:
```javascript
fetch('http://localhost:5000/api/health')
.then(r => r.json())
.then(d => console.log(d))
```
Should return:
```json
{
"status": "ok",
"ffmpeg_installed": true,
"timestamp": "2025-..."
}
```
### 5.2 Upload a Test Video
1. Drag and drop a video file
2. Check browser console for any errors
3. Verify backend terminal shows upload activity
---
## Common Issues & Solutions
### Issue 1: "Failed to fetch" Error
**Problem:** Frontend can't reach backend
**Solutions:**
1. **Check backend is running:**
```bash
ps aux | grep "python.*app.py"
```
2. **Test backend directly:**
```bash
curl http://localhost:5000/api/health
```
3. **Check CORS settings** in `backend/app.py`:
```python
CORS(app) # Should be present
```
4. **Verify port 5000 is not blocked:**
```bash
lsof -i :5000
```
---
### Issue 2: CORS Errors
**Symptom:** Console shows "CORS policy" errors
**Solution:** Ensure `backend/app.py` has:
```python
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
```
Already included! If still having issues, try:
```python
CORS(app, resources={r"/api/*": {"origins": "*"}})
```
---
### Issue 3: Backend Not Starting
**Error:** Port 5000 already in use
**Solution:**
```bash
# Find process using port 5000
lsof -ti:5000
# Kill it
lsof -ti:5000 | xargs kill
# Or use a different port
python app.py --port 5001
```
Then update `frontend/config.js`:
```javascript
API_BASE: 'http://localhost:5001/api'
```
---
### Issue 4: File Upload Fails
**Symptom:** Upload shows error or times out
**Check:**
1. **File size limit** (default 500MB):
```javascript
// In config.js
MAX_FILE_SIZE: 500 * 1024 * 1024
```
2. **Backend upload folder exists:**
```bash
ls -la backend/uploads/
```
3. **Permissions:**
```bash
chmod 755 backend/uploads
chmod 755 backend/outputs
```
---
## Alternative: Direct File Access
If you prefer not to use MAMP, you can open the HTML directly:
```bash
# Open with default browser
open frontend/index.html
# Or
# Just drag index.html into your browser
```
**Important:** Some browsers restrict `file://` protocol. Chrome/Safari should work fine.
---
## Development Workflow
### Typical Setup:
**Terminal 1 - Backend:**
```bash
cd /Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction
source venv/bin/activate
cd backend
python app.py
```
**MAMP:**
- Document Root: `.../frontend`
- Apache running on port 8888
- Access: http://localhost:8888/index.html
**Browser:**
- Open http://localhost:8888/index.html
- Open DevTools (F12) to monitor API calls
---
## Debugging Tips
### Enable Debug Mode
In `frontend/config.js`:
```javascript
DEBUG: true
```
In `backend/app.py`:
```python
app.run(debug=True, host='0.0.0.0', port=5000)
```
### Monitor Network Requests
1. Open browser DevTools (F12)
2. Go to **Network** tab
3. Upload a file
4. Check API calls to `localhost:5000`
### Check Backend Logs
Watch the terminal running `python app.py` for:
- Upload confirmations
- Conversion progress
- Error messages
---
## Production Considerations
For production deployment:
1. **Change API_BASE** in config.js to your production URL
2. **Disable DEBUG** mode
3. **Use production WSGI server** (Gunicorn, uWSGI)
4. **Set up proper CORS** restrictions
5. **Configure SSL/HTTPS**
6. **Implement authentication** if needed
---
## Quick Troubleshooting Commands
```bash
# Check if backend is running
curl http://localhost:5000/api/health
# Check if MAMP is serving files
curl http://localhost:8888/index.html
# Test backend upload endpoint
curl -X POST http://localhost:5000/api/platforms
# View backend logs
cd backend
python app.py # Watch terminal output
# Check FFmpeg
ffmpeg -version
# List processes
ps aux | grep -E "(python|httpd)"
```
---
## File Structure for MAMP
```
MAMP Document Root → /frontend/
├── index.html ← Entry point
├── style.css
├── app.js
└── config.js ← Configure API endpoint here
```
```
Python Backend → /backend/
├── app.py ← Must be running (port 5000)
├── video_processor.py
├── platform_specs.py
├── uploads/ ← Auto-created
└── outputs/ ← Auto-created
```
---
## Success Checklist
- [ ] Python backend running on port 5000
- [ ] MAMP Apache running
- [ ] MAMP Document Root set to `frontend/` folder
- [ ] `config.js` has correct API_BASE URL
- [ ] Can access http://localhost:8888/index.html
- [ ] Browser console shows no CORS errors
- [ ] Test upload works
- [ ] FFmpeg installed and detected
---
## Need Help?
1. Check backend terminal for errors
2. Check browser console (F12) for JavaScript errors
3. Test backend directly: `curl http://localhost:5000/api/health`
4. Verify MAMP is serving files: `curl http://localhost:8888/index.html`
**The key:** Python backend MUST be running for video processing to work!

282
PROJECT_SUMMARY.md Normal file
View file

@ -0,0 +1,282 @@
# 🎉 Project Complete: L'Oréal Video Optimizer
## ✅ Successfully Deployed & Backed Up
**Repository:** https://bitbucket.org/zlalani/loreal-video-optimizer
**Status:**
- ✅ Application working and tested
- ✅ Pushed to Bitbucket (main branch)
- ✅ MAMP-compatible setup verified
- ✅ All 21 platform configurations included
---
## 📊 Project Stats
- **Total Files:** 19
- **Lines of Code:** 3,062+
- **Platforms Supported:** 8
- **Platform Configurations:** 21
- **Codecs:** H264, H265, VP9
- **Languages:** Python, JavaScript, HTML, CSS
---
## 🎯 What Was Built
### Backend (Python Flask)
- REST API with 8 endpoints
- FFmpeg video conversion engine
- Platform specifications for all 21 configs
- File upload/download management
- Automatic codec selection
### Frontend (HTML/JS)
- Drag & drop interface
- Platform/aspect ratio auto-detection from filename
- Side-by-side video comparison player
- Black (#000000) + Yellow (#FFC407) theme
- Montserrat font throughout
- Real-time conversion progress
- File size reduction stats
### Documentation
- README.md - Full documentation
- QUICKSTART.md - 3-step start guide
- MAMP_SETUP.md - MAMP configuration
- START_WITH_MAMP.md - Quick MAMP guide
- TESTING.md - Testing procedures
- PROJECT_SUMMARY.md - This file
---
## 🎨 Design Specifications Met
- ✅ Black background (#000000)
- ✅ Yellow accent (#FFC407)
- ✅ Montserrat font from Google Fonts
- ✅ Dark theme with professional polish
- ✅ Responsive layout
- ✅ Clean, modern UI
---
## 📋 All 21 Platform Configurations
### Meta (4 configs)
- 1:1, 16:9, 4:5, 9:16 | H264 | 840-1400 kbps
### Pinterest (5 configs)
- 1:1, 16:9, 2:3, 4:5, 9:16 | H264 | 1100-1690 kbps
### Snapchat (2 configs)
- 16:9, 9:16 | H264 | 1100-1400 kbps
### TikTok (3 configs)
- 1:1, 16:9, 9:16 | H265 | 840-1300 kbps
### YouTube All Devices (4 configs)
- 1:1, 16:9, 4:5, 9:16 | VP9 | 1300-2000 kbps
### YouTube CTV (1 config)
- 16:9 | VP9 | 3300-7000 kbps
### Amazon Prime (1 config)
- 16:9 | H264 | 15000 kbps (minimum)
### Amazon Freevee (1 config)
- 16:9 | H264 | 4500-7000 kbps
---
## 🔧 Setup for Production Use
### Option 1: MAMP (Current Setup)
**Terminal 1 - Backend:**
```bash
cd /Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/backend
source ../venv/bin/activate
python app.py
```
**MAMP:**
- Document Root: `frontend` folder
- Access: http://localhost:8888/index.html
### Option 2: Standalone
```bash
./start.sh
```
Then open: http://localhost:8000
---
## 🧪 Testing Results
✅ Backend health check - Working
✅ Platform API endpoint - All 21 configs returned
✅ File upload - Tested successfully
✅ Video conversion - Working (multiple formats tested)
✅ Side-by-side comparison - Players sync correctly
✅ File download - Both original and optimized work
✅ Cleanup - Files delete properly
✅ CORS - No cross-origin issues with MAMP
✅ Filename detection - Auto-detects platform and aspect ratio
**Sample conversions tested:**
- Upload ✅
- Convert (TikTok 9:16) ✅
- Compare videos ✅
- Download optimized ✅
- Upload new file ✅
- Sync playback ✅
---
## 🎬 Features Implemented
### Core Features
- ✅ Drag & drop file upload
- ✅ Platform selection (8 platforms)
- ✅ Aspect ratio selection (per platform)
- ✅ Custom bitrate override (optional)
- ✅ FFmpeg conversion with optimal settings
- ✅ Progress indication during conversion
- ✅ Side-by-side video comparison
- ✅ File size reduction percentage
- ✅ Download both versions
- ✅ Synchronized playback controls
### Smart Features
- ✅ Filename pattern detection (auto-selects platform)
- ✅ Aspect ratio pattern detection
- ✅ Codec auto-selection based on platform
- ✅ Bitrate recommendations shown
- ✅ Video metadata display (resolution, duration, etc.)
- ✅ Format validation
### Developer Features
- ✅ CORS enabled for MAMP
- ✅ Debug mode available
- ✅ Comprehensive error handling
- ✅ RESTful API design
- ✅ Configurable endpoints (config.js)
- ✅ Proper .gitignore setup
---
## 📦 Repository Contents
```
loreal-video-optimizer/
├── backend/
│ ├── app.py (Flask server)
│ ├── video_processor.py (FFmpeg wrapper)
│ ├── platform_specs.py (21 configurations)
│ ├── api.php (PHP proxy - optional)
│ ├── requirements.txt
│ ├── uploads/ (temp uploads)
│ └── outputs/ (converted files)
├── frontend/
│ ├── index.html (UI)
│ ├── style.css (Black + Yellow theme)
│ ├── app.js (Frontend logic)
│ └── config.js (API configuration)
├── venv/ (Python virtual environment)
├── docs/
│ ├── README.md
│ ├── QUICKSTART.md
│ ├── MAMP_SETUP.md
│ ├── START_WITH_MAMP.md
│ ├── TESTING.md
│ └── PROJECT_SUMMARY.md
├── .gitignore
├── start.sh
└── Impact Plus PDF (source documentation)
```
---
## 🔑 Key Files to Know
**Configuration:**
- `frontend/config.js` - API endpoint URL
- `backend/platform_specs.py` - Platform settings
**Main Application:**
- `backend/app.py` - Backend server
- `frontend/index.html` - User interface
**Documentation:**
- `START_WITH_MAMP.md` - Start here!
- `MAMP_SETUP.md` - Detailed MAMP guide
---
## 🚀 Quick Commands
```bash
# Start backend
cd backend && source ../venv/bin/activate && python app.py
# Test backend
curl http://localhost:5000/api/health
# Check git status
git status
# Pull latest
git pull origin main
# Push changes
git add . && git commit -m "message" && git push
```
---
## 🎯 Based On
**L'Oréal CDMO Creative Optimization Documentation v1.1**
- Impact Plus - March 2025
- All specifications implemented from PDF
---
## 👤 Credits
**Repository Owner:** zlalani (Bitbucket)
**SSH Key:** djp1971
**Built with:** Claude Code
**Backend:** Python + Flask + FFmpeg
**Frontend:** HTML + JavaScript + CSS
---
## 📈 Next Steps (Optional Enhancements)
Future improvements could include:
- [ ] Batch processing multiple files
- [ ] Video preview before upload
- [ ] Quality presets (low/medium/high)
- [ ] Export conversion reports
- [ ] User authentication
- [ ] Job queue for large files
- [ ] Cloud storage integration
- [ ] Webhook notifications
- [ ] API rate limiting
- [ ] Production WSGI server setup
---
## ✅ Project Status: COMPLETE
**Date Completed:** October 16, 2025
**Status:** Production Ready
**Repository:** https://bitbucket.org/zlalani/loreal-video-optimizer
**Working:** Yes, tested and verified
🎉 **Ready to use!**

274
QUICKSTART.md Normal file
View file

@ -0,0 +1,274 @@
# Quick Start Guide
## 🚀 Get Started in 3 Steps
Choose your preferred setup method:
---
## Method 1: Standard Setup (Recommended)
### 1. Start the Application
**Option A - One Command:**
```bash
./start.sh
```
**Option B - Manual (Two Terminals):**
**Terminal 1 - Backend:**
```bash
cd backend
source ../venv/bin/activate
python app.py
```
**Terminal 2 - Frontend:**
```bash
cd frontend
python3 -m http.server 8000
```
### 2. Open Your Browser
Navigate to: **http://localhost:8000**
### 3. Upload and Convert
1. **Drag & drop** a video file onto the upload area
2. **Select platform** (e.g., TikTok, Meta, YouTube) - or let filename auto-detect
3. **Select aspect ratio** (e.g., 1:1, 16:9, 9:16) - or let filename auto-detect
4. ⚠️ **Check for yellow warning** if aspect ratio differs from original
5. Click **"Convert Video"**
6. **Compare** side-by-side and download optimized file
---
## Method 2: MAMP Setup
### 1. Start Python Backend
```bash
cd backend
source ../venv/bin/activate
python app.py
```
**Keep this terminal running!**
### 2. Configure MAMP
1. Open **MAMP** application
2. **Preferences** → **Web Server**
3. Set **Document Root** to: `[path-to]/Loreal-File-Reduction/frontend`
4. Click **"Start Servers"**
### 3. Open Your Browser
Navigate to: **http://localhost:8888/index.html**
(Or your MAMP port)
---
## 📝 Filename Auto-Detection
Name your files with these patterns for automatic detection:
### Examples
| Filename | Auto-Detects |
|----------|-------------|
| `summer_campaign_tiktok_9x16.mp4` | TikTok + 9:16 |
| `product_ad_meta_1x1_v2.mov` | Meta + 1:1 |
| `youtube_ctv_16x9_final.mp4` | YouTube CTV + 16:9 |
| `pinterest_2x3_inspiration.mp4` | Pinterest + 2:3 |
### Platform Keywords
- **TikTok:** `_tiktok_`, `_tt_`
- **Meta:** `_meta_`, `_fb_`, `_ig_`
- **YouTube:** `_youtube_`, `_yt_`
- **YouTube CTV:** `_youtube_ctv_`, `_yt_ctv_`, `_ctv_`
- **Pinterest:** `_pinterest_`, `_pin_`
- **Snapchat:** `_snapchat_`, `_snap_`
- **Amazon Prime:** `_prime_`, `_amazon_prime_`
- **Amazon Freevee:** `_freevee_`, `_amazon_freevee_`
### Aspect Ratio Keywords
- **1:1**`_1x1_`, `_square_`
- **16:9**`_16x9_`, `_landscape_`
- **9:16**`_9x16_`, `_vertical_`, `_portrait_`
- **4:5**`_4x5_`
- **2:3**`_2x3_`
---
## 🎯 Platform Specifications
| Platform | Best For | Codec | Popular Sizes |
|----------|----------|-------|---------------|
| **TikTok** | Vertical mobile | H265 | 540×960, 640×640 |
| **Meta** | All devices | H264 | 720×720, 1280×720 |
| **YouTube** | Quality focus | VP9 | 1280×720, 1920×1080 |
| **Pinterest** | Inspiration | H264 | 1000×1500 (2:3) |
| **Snapchat** | Stories | H264 | 720×1280 |
| **Amazon** | CTV/Streaming | H264 | 1920×1080 |
---
## ✨ Key Features
### During Configuration:
- ⚠️ **Yellow Warning** - Shows when aspect ratio differs from original
- **Auto-detection** - Platform and aspect ratio from filename
- **Bitrate recommendations** - Optimal ranges shown for each platform
### During Comparison:
- 🔴 **Red Warning** - Indicates aspect ratio was changed
- 🔴 **Red Outline** - On optimized video when aspect ratio differs
- **Video Specs** - Complete details for both videos shown below players
- **Mute Controls** - Independent audio control for each video
- **Sync Playback** - Play both videos simultaneously
- **File Size Stats** - Shows reduction percentage
### Video Specifications Shown:
- Platform (target platform for optimized)
- Resolution (e.g., 1920×1080)
- Aspect Ratio (e.g., 16:9)
- Codec (H264, H265, VP9)
- Bitrate (e.g., 1500k)
- Duration
- File Size
---
## 🛠 Troubleshooting
### Backend won't start?
**Check if something is on port 5000:**
```bash
lsof -ti:5000
```
**Kill it:**
```bash
lsof -ti:5000 | xargs kill
```
**Then restart:**
```bash
cd backend
source ../venv/bin/activate
python app.py
```
### FFmpeg not found?
**Install FFmpeg:**
```bash
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt-get install ffmpeg
```
**Verify installation:**
```bash
ffmpeg -version
```
### Port already in use?
**Standard Setup - Port 8000:**
```bash
lsof -ti:8000 | xargs kill
```
**MAMP - Port 8888:**
Check MAMP settings or change port in Preferences
### "Failed to fetch" error?
**Checklist:**
1. ✅ Backend running? (`curl http://localhost:5000/api/health`)
2. ✅ Frontend accessible? (Open http://localhost:8000)
3. ✅ Check browser console (F12) for errors
4. ✅ Verify `frontend/config.js` has correct API_BASE
---
## 📊 Expected Results
### File Size Reduction
Typical results:
| Platform | Reduction |
|----------|-----------|
| **TikTok** (H265) | 30-40% |
| **Meta** (H264) | 25-35% |
| **YouTube** (VP9) | 20-30% |
### Conversion Times
For 1 minute 1080p video:
| Platform | Time |
|----------|------|
| **Meta/Pinterest** | 20-40 seconds |
| **TikTok** | 40-80 seconds |
| **YouTube** | 60-120 seconds |
*Times vary based on hardware*
---
## 🎨 Color Scheme
- **Primary:** Black (#000000)
- **Accent:** Yellow (#FFC407)
- **Font:** Montserrat
---
## 📚 More Information
- **README.md** - Complete documentation
- **MAMP_SETUP.md** - Detailed MAMP configuration
- **START_WITH_MAMP.md** - Quick MAMP instructions
- **TESTING.md** - Testing procedures
---
## 🚦 Status Indicators
### Yellow Warning (Configuration Page)
- Appears when selected aspect ratio ≠ original
- Prevents accidental distortion
- Shows **before** conversion
### Red Warning (Comparison Page)
- Appears when aspect ratio **was** changed
- Red outline on optimized video
- Visual alert for quality check
---
## 💡 Tips
1. **Use descriptive filenames** with platform and aspect ratio for auto-detection
2. **Check yellow warning** before converting to avoid distortion
3. **Use mute controls** to compare audio quality
4. **Compare videos side-by-side** before downloading
5. **Watch for red indicators** if aspect ratio was changed
---
**Need help?** Check the full README.md for detailed documentation.
**Repository:** https://bitbucket.org/zlalani/loreal-video-optimizer

774
README.md
View file

@ -1,461 +1,539 @@
# Video Optimizer for Social Media Platforms
Creative video optimization tool built for CDMO workflows. Converts videos to platform-specific specifications using FFmpeg, with two operating modes: an interactive web UI and a fully automated Box.com pipeline.
L'Oréal Creative Optimization Tool - Based on L'Oréal CDMO Creative Optimization Documentation v1.1
---
## Overview
## Modes of Operation
This application optimizes video files for various social media platforms using platform-specific codecs and bitrate recommendations to minimize file sizes while maintaining quality.
| Mode | How it works | When to use |
|------|-------------|-------------|
| **Web UI** | User uploads video → selects platform → downloads optimised file | Ad-hoc individual conversions |
| **Box Automation** | Drop video in Box `IN` folder → auto-processes → delivers to `OUT_SUCCESS` | Batch / unattended workflows |
### Key Features
- **21 Platform Configurations** across 8 social media platforms
- **Automatic filename detection** for platform and aspect ratio
- **Side-by-side video comparison** with synchronized playback
- **Independent mute controls** for each video player
- **Detailed video specifications** display (codec, bitrate, resolution, etc.)
- **Aspect ratio mismatch warnings** with visual indicators
- **FFmpeg-powered conversion** with optimal codec settings
- **File size reduction tracking** with percentage display
- **Drag & drop interface** with progress indication
---
## Supported Platforms
| Platform | Codec | Aspect Ratios | Bitrate Range |
|----------|-------|---------------|---------------|
| **Meta (Facebook/Instagram)** | H264 | 1:1, 16:9, 4:5, 9:16 | 8401400 kbps |
| **Pinterest** | H264 | 1:1, 16:9, 2:3, 4:5, 9:16 | 11001690 kbps |
| **Snapchat** | H264 | 16:9, 9:16 | 11001400 kbps |
| **TikTok** | H265 | 1:1, 16:9, 9:16 | 8401300 kbps |
| **YouTube & DV360** | VP9 | 1:1, 16:9, 4:5, 9:16 | 13002000 kbps |
| **YouTube CTV** | VP9 | 16:9 | 33007000 kbps |
| **Amazon Prime** | H264 | 16:9 | 15000 kbps fixed |
| **Amazon Freevee** | H264 | 16:9 | 45007000 kbps |
---
## Prerequisites
- **Python 3.8+**
- **FFmpeg** — required for all video processing
```bash
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt-get install ffmpeg
```
| Platform | Codec | Aspect Ratios | Bitrate Range | Notes |
|----------|-------|---------------|---------------|-------|
| **Meta (Facebook/Instagram)** | H264 | 1:1, 16:9, 4:5, 9:16 | 840-1400 kbps | Mobile optimized |
| **Pinterest** | H264 | 1:1, 16:9, 2:3, 4:5, 9:16 | 1100-1690 kbps | 2:3 for inspiration boards |
| **Snapchat** | H264 | 16:9, 9:16 | 1100-1400 kbps | Stories format |
| **TikTok** | H265 | 1:1, 16:9, 9:16 | 840-1300 kbps | Recommended for quality |
| **YouTube & DV360** | VP9 | 1:1, 16:9, 4:5, 9:16 | 1300-2000 kbps | All devices |
| **YouTube CTV** | VP9 | 16:9 | 3300-7000 kbps | Connected TV specific |
| **Amazon Prime** | H264 | 16:9 | 15000 kbps | Minimum bitrate required |
| **Amazon Freevee** | H264 | 16:9 | 4500-7000 kbps | CTV specific |
---
## Installation
### Prerequisites
1. **Python 3.8+**
2. **FFmpeg** - Required for video processing
#### Install FFmpeg
**macOS:**
```bash
# Clone and create virtual environment
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r backend/requirements.txt
brew install ffmpeg
```
**Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install ffmpeg
```
**Windows:**
Download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to PATH
---
## Configuration
## Quick Start (Standard Setup)
Copy the example env file and fill in your credentials:
### Option 1: One-Command Start
```bash
cp .env.example .env
```
### Required: Microsoft SSO (Azure AD)
```env
AZURE_CLIENT_ID=your-client-id
AZURE_TENANT_ID=your-tenant-id
REDIRECT_URI=http://localhost:3000
```
Create an app registration in [Azure Portal](https://portal.azure.com):
- Supported account types: **Single tenant**
- Redirect URI platform: **Single-page application (SPA)**
- Add URIs: `http://localhost:3000` and `http://localhost:3000/admin.html`
- Grant **Microsoft Graph → User.Read** permission
### Optional: Box.com Automation
```env
BOX_CONFIG_PATH=../oliver_box_config.json
BOX_VIDEO_OPTIMIZER_FOLDER_ID=362124323515
BOX_AS_USER_ID= # leave blank — service account has direct access
BOX_WEBHOOK_SECRET= # only needed for webhook mode
BOX_USE_POLLING=false # set true for dev/testing (no public URL needed)
BOX_POLL_INTERVAL_SECONDS=60
BOX_PROCESSOR_PORT=5001
```
---
## Running the Web UI
```bash
# One-command start (backend + frontend)
./start.sh
# Or manually in two terminals:
# Terminal 1 — backend (port 5000)
cd backend && python app.py
# Terminal 2 — frontend (port 3000)
cd frontend && python3 -m http.server 3000
```
Open **http://localhost:3000** and sign in with your Microsoft account.
This starts both backend (port 5000) and frontend (port 8000) automatically.
Admin panel: **http://localhost:3000/admin.html**
### Option 2: Manual Start
**Terminal 1 - Backend:**
```bash
# Activate virtual environment
source venv/bin/activate # On Windows: venv\Scripts\activate
# Start backend
cd backend
python app.py
```
**Terminal 2 - Frontend:**
```bash
# Serve frontend
cd frontend
python3 -m http.server 8000
```
Then open: **http://localhost:8000**
---
## Running the Box Automation Service
## MAMP Setup
### Step 1 — Verify setup (run once)
For users who prefer MAMP for frontend hosting:
### 1. Start Python Backend
```bash
cd backend
python box_setup.py
source ../venv/bin/activate
python app.py
```
This authenticates with Box, lists accessible folders, discovers `IN` / `OUT_SUCCESS` / `OUT_FAILED` sub-folders and prints a full configuration summary.
**Keep this terminal running!** Backend must be active on port 5000.
### Step 2 — Start the service
### 2. Configure MAMP
Box automation runs inside the same `app.py` process as the web UI — no separate service needed.
1. Open **MAMP** application
2. Go to **Preferences** → **Web Server**
3. Set **Document Root** to:
```
/Users/[your-path]/Loreal-File-Reduction/frontend
```
4. Click **Start Servers**
**Polling mode** (recommended for development — no public URL needed):
```bash
# Set in .env: BOX_USE_POLLING=true
./start.sh # or: python backend/app.py
```
### 3. Access Application
**Webhook mode** (for production):
```bash
# Expose local port via ngrok (development)
ngrok http 5000
Navigate to: **http://localhost:8888/index.html**
# Start the app as normal
python backend/app.py
```
Configure a Box webhook in the Box Admin Console:
- Target URL: `https://<your-url>/webhooks/box`
- Trigger: `FILE.UPLOADED` on the `IN` folder
- Copy the webhook secret into `BOX_WEBHOOK_SECRET` in `.env`
(Or use your MAMP port if different)
### Step 3 — Test a file
Upload a video with a valid naming convention to the Box `IN` folder, or trigger manually:
```bash
curl -X POST http://localhost:5001/manual-trigger \
-H "Content-Type: application/json" \
-d '{"file_id": "PASTE_BOX_FILE_ID", "filename": "campaign_tiktok_9x16.mp4"}'
```
**See MAMP_SETUP.md for detailed MAMP configuration and troubleshooting.**
---
## Box Naming Convention
## Usage Guide
Filenames **must** include both a platform and an aspect ratio pattern for the automation to process them. Files without both patterns are skipped and an error report is placed in `OUT_FAILED`.
### 1. Upload Video
### Platform Patterns
| Platform | Patterns |
|----------|---------|
| Meta | `_meta_`, `_fb_`, `_ig_`, `_facebook_`, `_instagram_` |
| Pinterest | `_pinterest_`, `_pin_` |
| Snapchat | `_snapchat_`, `_snap_` |
| TikTok | `_tiktok_`, `_tt_` |
| YouTube | `_youtube_`, `_yt_` |
| YouTube CTV | `_youtube_ctv_`, `_yt_ctv_`, `_ctv_` |
| Amazon Prime | `_prime_`, `_amazon_prime_` |
| Amazon Freevee | `_freevee_`, `_amazon_freevee_` |
- **Drag and drop** a video file onto the upload area
- Or **click to browse** and select a file
- Supported formats: **MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V**
- Max file size: **500MB** (configurable in backend/app.py)
### Aspect Ratio Patterns
| Ratio | Patterns |
|-------|---------|
| 1:1 | `_1x1_`, `_square_` |
| 16:9 | `_16x9_`, `_landscape_` |
| 9:16 | `_9x16_`, `_vertical_`, `_portrait_` |
| 4:5 | `_4x5_` |
| 2:3 | `_2x3_` |
### 2. Automatic Detection
**Valid examples:**
```
campaign_tiktok_9x16.mp4 → TikTok 9:16
product_meta_1x1_v2.mov → Meta 1:1
hero_youtube_ctv_16x9_final.mp4 → YouTube CTV 16:9
```
The app auto-detects platform and aspect ratio from filename patterns:
#### Platform Patterns
- **Meta:** `_meta_`, `_fb_`, `_ig_`, `_facebook_`, `_instagram_`
- **Pinterest:** `_pinterest_`, `_pin_`
- **Snapchat:** `_snapchat_`, `_snap_`
- **TikTok:** `_tiktok_`, `_tt_`
- **YouTube:** `_youtube_`, `_yt_`
- **YouTube CTV:** `_youtube_ctv_`, `_yt_ctv_`, `_ctv_`
- **Amazon Prime:** `_prime_`, `_amazon_prime_`
- **Amazon Freevee:** `_freevee_`, `_amazon_freevee_`
#### Aspect Ratio Patterns
- **1:1** - `_1x1_`, `_square_`, `_1-1_`
- **16:9** - `_16x9_`, `_landscape_`, `_16-9_`
- **4:5** - `_4x5_`, `_4-5_`
- **9:16** - `_9x16_`, `_vertical_`, `_9-16_`, `_portrait_`
- **2:3** - `_2x3_`, `_2-3_`
#### Example Filenames
- `summer_campaign_tiktok_9x16.mp4` → Auto-detects **TikTok** + **9:16**
- `product_ad_meta_1x1_final.mov` → Auto-detects **Meta** + **1:1**
- `youtube_16x9_v2.mp4` → Auto-detects **YouTube** + **16:9**
### 3. Configure Settings
- Select **platform** (if not auto-detected)
- Select **aspect ratio** (if not auto-detected)
- ⚠️ **Yellow warning** appears if aspect ratio differs from original
- Optionally override **bitrate** (recommended values shown)
- View platform specifications and codec requirements
### 4. Convert Video
- Click **"Convert Video"** button
- Progress bar shows conversion status
- FFmpeg processes video with optimal settings
### 5. Compare & Download
#### Video Comparison Features:
- **Side-by-side playback** of original and optimized videos
- **Detailed specifications** for each video:
- Platform, Resolution, Aspect Ratio
- Codec, Bitrate, Duration, File Size
- **File size reduction** percentage displayed
- **Independent mute controls** for each player
- **Synchronized playback** button to play both simultaneously
- **Pause both** button for simultaneous pause
#### Aspect Ratio Changed Indicators:
- 🔴 **Red warning message** appears at top of comparison
- 🔴 **Red outline** with glow effect on optimized video
- Clear indication when video was converted to different aspect ratio
#### Download Options:
- Download **original** video
- Download **optimized** video
- Upload new file to start over
---
## Box Automation Pipeline
## Features in Detail
When a valid file is detected in the `IN` folder:
### Aspect Ratio Mismatch Warnings
1. Filename validated (platform + aspect ratio must be detectable)
2. Video downloaded from Box to server temp directory
3. FFmpeg conversion with platform-specific codec settings
4. JSON report generated with full before/after metadata
5. Optimised video uploaded to `OUT_SUCCESS`
6. JSON report uploaded to `OUT_SUCCESS` alongside the video
7. Original file deleted from `IN`
8. Conversion logged to daily audit log (`backend/logs/conversions/`)
9. Temp files cleaned up
**Configuration Page (Yellow Warning):**
- Appears when selected aspect ratio differs from original
- Warns about potential distortion before conversion
- Helps prevent accidental aspect ratio changes
On failure or invalid filename, an error report JSON is placed in `OUT_FAILED`.
**Comparison Page (Red Warning):**
- Red warning message at top of comparison section
- Red outline and glow effect on optimized video player
- Clear visual indication that aspect ratio was modified
- Alerts user to check for distortion, stretching, or cropping
### Box Folder Structure
### Video Specifications Display
```
VIDEO_OPTIMIZER/ (ID: 362124323515)
├── IN/ (ID: 362125331342) ← drop raw videos here
├── OUT_SUCCESS/ (ID: 362127206346) ← optimised video + report.json
└── OUT_FAILED/ (ID: 362119054851) ← error_report.json
```
Below each video player, detailed specifications are shown:
### Success Report Format (`*_report.json`)
**Original Video:**
- Platform: Unknown
- Resolution (e.g., 1920×1080)
- Aspect Ratio (e.g., 16:9)
- Codec (e.g., H264)
- Bitrate (e.g., 5000 kbps)
- Duration
- File Size
```json
{
"status": "success",
"timestamp": "2026-02-20T14:32:15",
"processing_time_seconds": 48.3,
"original_file": {
"filename": "campaign_tiktok_9x16.mp4",
"size_mb": 47.6,
"codec": "h264",
"resolution": "1920x1080",
"bitrate": "8000k",
"duration_seconds": 30.5
},
"optimised_file": {
"filename": "campaign_tiktok_9x16_optimized.mp4",
"size_mb": 12.1,
"size_reduction_percent": 74.6,
"savings_mb": 35.5
},
"conversion_details": {
"platform": "tiktok",
"aspect_ratio": "9:16",
"resolution": "1080x1920",
"codec": "libx265",
"bitrate": "1000k"
}
}
```
**Optimized Video:**
- Target Platform (e.g., TikTok)
- Output Resolution
- Aspect Ratio
- Codec used
- Applied Bitrate
- Duration
- File Size
### Mute Controls
- Independent **Mute/Unmute** button for each video
- Listen to one video at a time or both together
- Useful for audio quality comparison
- Visual indication (button changes when muted)
---
## Web UI Usage
### 1. Upload
Drag and drop or browse for a video (MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V — max 500MB).
### 2. Auto-detection
Platform and aspect ratio are detected from the filename. Override via dropdowns if needed.
### 3. Convert
Click **Convert Video**. FFmpeg processes with platform-optimised settings.
### 4. Compare & Download
- Side-by-side playback of original vs optimised
- Synchronized and independent playback controls
- Mute controls for each player
- Full codec/bitrate/resolution specs for both videos
- File size reduction percentage
- Download either version
### Aspect Ratio Warnings
- **Yellow** (config page): selected ratio differs from original
- **Red** (comparison page): conversion changed the aspect ratio
---
## API Reference
### Main Backend (port 5000)
## API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/health` | GET | FFmpeg status check |
| `/api/config` | GET | Azure AD config for frontend SSO |
| `/api/platforms` | GET | All platform specifications |
| `/api/detect` | POST | Detect platform/ratio from filename |
| `/api/health` | GET | Health check and FFmpeg status |
| `/api/platforms` | GET | Get all platform specifications |
| `/api/detect` | POST | Detect platform from filename |
| `/api/upload` | POST | Upload video file |
| `/api/convert` | POST | Convert to target platform/format |
| `/api/stream/<type>/<id>` | GET | Stream video for browser playback |
| `/api/download/<type>/<id>` | GET | Download original or optimised |
| `/api/cleanup/<id>` | DELETE | Delete files after session |
### Admin (port 5000)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/admin/platforms` | POST/PUT/DELETE | Manage platform specs |
| `/api/admin/export` | GET | Export all specs as JSON |
| `/api/admin/import` | POST | Import specs from JSON |
| `/api/admin/reset-factory` | POST | Restore factory defaults |
| `/api/admin/naming-conventions` | GET/POST | Manage detection patterns |
### Box Automation (port 5000 — same as main API)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/webhooks/box` | POST | Receive Box webhook events |
| `/api/box/trigger` | POST | Manually trigger processing `{file_id, filename}` |
| `/api/box/health` | GET | Box initialisation status + folder config |
| `/api/convert` | POST | Convert video to target format |
| `/api/stream/<type>/<id>` | GET | Stream video for playback |
| `/api/download/<type>/<id>` | GET | Download original or optimized file |
| `/api/cleanup/<id>` | DELETE | Delete uploaded and converted files |
---
## Project Structure
```
loreal-video-optimizer/
Loreal-File-Reduction/
├── backend/
│ ├── app.py # Flask API + Box automation (single process, port 5000)
│ ├── video_processor.py # FFmpeg conversion engine
│ ├── platform_specs.py # 21 platform configurations + detection
│ ├── conversion_logger.py # Daily JSON audit logging
│ ├── file_cleanup.py # File retention management
│ ├── box_client.py # Box SDK wrapper (JWT auth, upload/download)
│ ├── box_processor.py # BoxProcessor + BoxPoller classes (imported by app.py)
│ ├── box_setup.py # Setup diagnostic script
│ ├── test_box_processor.py # Box automation test suite
│ ├── requirements.txt
│ ├── uploads/ # Temp upload storage (auto-cleaned)
│ ├── outputs/ # Temp converted output (auto-cleaned)
│ └── logs/conversions/ # Daily conversion audit logs
│ ├── app.py # Flask REST API server
│ ├── video_processor.py # FFmpeg conversion engine
│ ├── platform_specs.py # 21 platform configurations
│ ├── requirements.txt # Python dependencies
│ ├── uploads/ # Temporary upload storage
│ └── outputs/ # Converted video output
├── frontend/
│ ├── index.html # Main UI (requires Microsoft SSO)
│ ├── admin.html # Admin panel (requires Microsoft SSO)
│ ├── app.js # Upload, convert, compare logic
│ ├── admin.js # Admin CRUD operations
│ ├── auth.js # MSAL Browser SSO wrapper
│ ├── config.js # API base URL (auto-detects env)
│ ├── style.css # Main styles
│ └── admin.css # Admin styles
├── deployment/
│ ├── deploy.sh # Production deployment automation
│ ├── video-optimizer-backend.service # Systemd: main backend
│ ├── box-processor.service # Systemd: Box automation service
│ └── apache-complete.conf # Apache vhost config
├── oliver_box_config.json # Box JWT credentials (gitignored)
├── .env # Environment variables (gitignored)
├── .env.example # Environment variable template
├── start.sh # Dev startup script
└── CLAUDE.md # AI assistant project instructions
│ ├── index.html # Main user interface
│ ├── style.css # Black + #FFC407 theme
│ ├── app.js # Frontend application logic
│ └── config.js # API endpoint configuration
├── venv/ # Python virtual environment
├── docs/
│ ├── README.md # This file
│ ├── QUICKSTART.md # Quick start guide
│ ├── MAMP_SETUP.md # Detailed MAMP setup
│ ├── START_WITH_MAMP.md # Quick MAMP guide
│ ├── TESTING.md # Testing procedures
│ └── PROJECT_SUMMARY.md # Project overview
├── .gitignore
├── start.sh # One-command startup script
└── Impact Plus PDF # Source documentation
```
---
## Codec Settings Reference
## Technical Specifications
**H264** (Meta, Pinterest, Snapchat, Amazon):
- `preset=medium`, `crf=23`, `profile=main`, `pix_fmt=yuv420p`
### Codec Settings
**H265** (TikTok):
- `preset=medium`, `crf=28`, `pix_fmt=yuv420p`
**H264 (Meta, Pinterest, Snapchat, Amazon):**
- Preset: medium
- CRF: 23
- Profile: main
- Pixel format: yuv420p
**VP9** (YouTube, YouTube CTV):
- `deadline=good`, `cpu-used=2`, `row-mt=1`
**H265 (TikTok):**
- Preset: medium
- CRF: 28
- Pixel format: yuv420p
**Audio:**
- AAC 128 kbps (mobile platforms)
- AAC/Opus 192 kbps (CTV platforms)
**VP9 (YouTube):**
- Deadline: good
- CPU-used: 2
- Row-mt: enabled
### Audio Settings
- **AAC codec:** 128 kbps for mobile, 192 kbps for CTV
- **Opus codec:** 128-192 kbps for VP9/YouTube
---
## Configuration
### Change API Endpoint
Edit `frontend/config.js`:
```javascript
const CONFIG = {
API_BASE: 'http://localhost:5000/api', // ← Change this
MAX_FILE_SIZE: 500 * 1024 * 1024,
DEBUG: true
};
```
### Adjust File Size Limit
Edit `backend/app.py`:
```python
MAX_FILE_SIZE = 500 * 1024 * 1024 # Change this value (bytes)
```
---
## Troubleshooting
### Box automation not working
### Backend Won't Start
**Issue:** Port 5000 already in use
**Solution:**
```bash
# Run diagnostic script
python backend/box_setup.py
# Find and kill process on port 5000
lsof -ti:5000 | xargs kill
# Check Box status via API
curl http://localhost:5000/api/box/health
# Check BOX_VIDEO_OPTIMIZER_FOLDER_ID is set in .env
# Verify oliver_box_config.json exists in project root
# Or use a different port
python app.py --port 5001 # Then update frontend/config.js
```
### Port already in use
```bash
lsof -ti:5000 | xargs kill # Kills both web API and Box automation (same process)
```
### FFmpeg Not Found
### FFmpeg not found
**Verify installation:**
```bash
ffmpeg -version
brew install ffmpeg # macOS
```
### Box file not being processed
- Check filename includes both platform and aspect ratio pattern
- Verify the service is running (`/health` endpoint)
- Check `OUT_FAILED` folder for error report
- In polling mode, check logs for `[POLLER]` output
### SSO login fails
- Verify `AZURE_CLIENT_ID` and `AZURE_TENANT_ID` in `.env`
- Confirm redirect URIs match in Azure AD app registration
- Backend must be running for `/api/config` to be accessible
---
## Production Deployment
Services are managed via systemd on the production server:
**If not installed:**
```bash
# Main backend
sudo systemctl start video-optimizer-backend
sudo systemctl enable video-optimizer-backend
# macOS
brew install ffmpeg
# Box automation service
sudo systemctl start box-processor
sudo systemctl enable box-processor
# Ubuntu/Debian
sudo apt-get install ffmpeg
# View logs
sudo journalctl -u video-optimizer-backend -f
sudo journalctl -u box-processor -f
# Windows - Download from ffmpeg.org and add to PATH
```
See `deployment/` folder for full deployment scripts and Apache configuration.
### Upload Fails
**Check:**
1. File size under 500MB (or configured limit)
2. Backend server is running
3. `backend/uploads/` folder exists and is writable
4. File format is supported (MP4, MOV, etc.)
### CORS Errors
**Verify** `backend/app.py` has:
```python
from flask_cors import CORS
CORS(app)
```
Already included! If issues persist, check browser console for details.
### Video Won't Play
**Try:**
1. Refresh page
2. Check backend logs for conversion errors
3. Verify FFmpeg is installed and working
4. Try a different browser (Chrome/Firefox/Safari)
---
## Design
- **Colors:** Black (`#000000`) background + Yellow (`#FFC407`) accents
- **Colors:** Black (#000000) + Yellow (#FFC407)
- **Font:** Montserrat (Google Fonts)
- **Theme:** Dark UI
- **Theme:** Dark UI with yellow accents
- **Layout:** Responsive, mobile-friendly design
---
## Performance
### Expected Conversion Times
1 minute 1080p video (varies by hardware):
| Platform | Codec | Expected Time |
|----------|-------|---------------|
| Meta/Pinterest | H264 | 20-40 seconds |
| TikTok | H265 | 40-80 seconds |
| YouTube | VP9 | 60-120 seconds |
### File Size Reduction
Typical reduction rates:
| Platform | Expected Reduction |
|----------|-------------------|
| TikTok (H265) | 30-40% |
| Meta (H264) | 25-35% |
| YouTube (VP9) | 20-30% |
*Actual results vary based on source quality and content complexity*
---
## Documentation
- **README.md** - Complete documentation (this file)
- **QUICKSTART.md** - 3-step start guide
- **MAMP_SETUP.md** - Detailed MAMP configuration
- **START_WITH_MAMP.md** - Quick MAMP instructions
- **TESTING.md** - Testing procedures and validation
- **PROJECT_SUMMARY.md** - Project overview and stats
---
## Based On
**L'Oréal CDMO Creative Optimization Documentation v1.1**
All platform specifications, codecs, and bitrate recommendations are sourced from official L'Oréal documentation for creative optimization and environmental impact reduction.
---
## Repository
**Bitbucket:** https://bitbucket.org/zlalani/loreal-video-optimizer
---
## Development
### Running in Development Mode
Backend with debug mode:
```bash
cd backend
source ../venv/bin/activate
python app.py # Debug mode is enabled by default
```
Frontend with hot reload - use your preferred tool or simple HTTP server.
### Adding New Platforms
Edit `backend/platform_specs.py`:
1. Add platform configuration to `PLATFORM_SPECS` dictionary
2. Add filename patterns to `FILENAME_PATTERNS`
3. Backend will automatically expose via API
---
## Production Deployment
For production use:
1. **Use production WSGI server** (Gunicorn, uWSGI)
```bash
gunicorn -w 4 -b 0.0.0.0:5000 app:app
```
2. **Set up reverse proxy** (Nginx, Apache)
3. **Configure SSL/HTTPS**
4. **Disable debug mode** in `backend/app.py`:
```python
app.run(debug=False, host='0.0.0.0', port=5000)
```
5. **Implement authentication** if needed
6. **Set up file cleanup cron job** for uploads/outputs folders
7. **Configure proper CORS** restrictions for production domain
---
## Credits
- **Repository Owner:** zlalani (Bitbucket)
- **Built with:** Claude Code
- **Backend:** Python + Flask + FFmpeg
- **Frontend:** HTML + JavaScript + CSS
- **Documentation Source:** L'Oréal CDMO Creative Optimization Documentation v1.1
---
## License
Internal tool for L'Oréal creative optimization workflows.
---
## Version
**Current Version:** 2.0
**Last Updated:** February 2026
**Current Version:** 1.0
**Last Updated:** October 2025
**Phase 1 — Web UI:**
- Microsoft SSO authentication
- 21 platform configurations across 8 platforms
- Automatic filename-based platform/aspect ratio detection
- Side-by-side video comparison with synchronized playback
- Admin panel with full CRUD for platforms and naming conventions
- Conversion audit logging
**Phase 2 — Box.com Automation:**
- Automated pipeline triggered by Box webhook or polling
- JWT service account authentication (`oliver_box_config.json`)
- 8-step processing pipeline with full error handling
- Original file deleted from `IN` after successful processing
**Phase 3 — Automated Reporting:**
- JSON report generated per conversion with full metadata
- Original video metadata (codec, resolution, bitrate) captured
- Reports delivered to `OUT_SUCCESS` alongside optimised video
- Error reports delivered to `OUT_FAILED` for invalid/failed files
**Recent Updates:**
- ✅ Independent mute controls for video comparison
- ✅ Detailed video specifications display
- ✅ Aspect ratio mismatch warnings (yellow + red)
- ✅ Red outline indicator for aspect ratio changes
- ✅ Platform field alignment in specifications
- ✅ Enhanced user experience and visual feedback

203
START_WITH_MAMP.md Normal file
View file

@ -0,0 +1,203 @@
# 🚀 Start with MAMP - Quick Guide
## What You Need Running
The app needs **TWO** things running at the same time:
1. **Python Backend** (video processing) - Port 5000
2. **MAMP Frontend** (user interface) - Port 8888 (or your MAMP port)
---
## Step 1: Start Python Backend
Open **Terminal** and run:
```bash
cd /Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/backend
source ../venv/bin/activate
python app.py
```
**Leave this terminal open!** You should see:
```
Starting Video Optimization Server...
* Running on http://127.0.0.1:5000
```
---
## Step 2: Configure MAMP
1. Open **MAMP** application
2. Click **Preferences** → **Web Server**
3. Set **Document Root** to:
```
/Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/frontend
```
4. Click **OK**
5. Click **Start Servers**
---
## Step 3: Open in Browser
Go to: **http://localhost:8888/index.html**
(Or use your MAMP port if different)
---
## ✅ It's Working When:
- You see the black page with yellow "Video Optimizer" title
- You can drag & drop a video file
- After uploading, you see platform options (Meta, TikTok, YouTube, etc.)
- Video specifications display below each player after conversion
- Mute buttons work independently for each video
---
## ✨ New Features You'll See
### 1. Aspect Ratio Warnings
**Yellow Warning (Page 1 - Configuration):**
- Shows when you select an aspect ratio different from your original video
- Helps prevent accidental distortion **before** converting
**Red Warning (Page 2 - Comparison):**
- Shows when aspect ratio **was** changed during conversion
- Red outline with glow effect around optimized video
- Visual alert to check for distortion
### 2. Video Specifications
Below each video player:
- **Platform** - Shows target platform (Optimized) or "Unknown" (Original)
- **Resolution** - Exact pixel dimensions
- **Aspect Ratio** - Format ratio (16:9, 1:1, etc.)
- **Codec** - Video codec used (H264, H265, VP9)
- **Bitrate** - Video bitrate in kbps
- **Duration** - Video length
- **File Size** - Total file size
### 3. Mute Controls
- **🔊 Mute/🔇 Unmute** button for each video
- Listen to one video at a time or both together
- Great for comparing audio quality
- Button highlights when muted
### 4. File Size Reduction
At the top of comparison page:
- Original file size
- Optimized file size
- **Reduction percentage** (typically 20-40%)
---
## ❌ Troubleshooting "Failed to fetch"
### Problem: Upload fails with "Failed to fetch"
**Check 1:** Is Python backend running?
```bash
curl http://localhost:5000/api/health
```
Should return:
```json
{
"status": "ok",
"ffmpeg_installed": true
}
```
**Check 2:** Open browser console (F12) and look for errors
**Check 3:** Make sure `frontend/config.js` has:
```javascript
API_BASE: 'http://localhost:5000/api'
```
---
## Quick Test
### Test Backend Only:
```bash
# In terminal
curl http://localhost:5000/api/platforms
```
Should show a JSON response with platform data.
### Test Frontend:
Open http://localhost:8888/index.html in browser - page should load with black background and yellow title.
---
## Stop Everything
### Stop Backend:
Press `Ctrl+C` in the terminal running Python
### Stop MAMP:
Click "Stop Servers" in MAMP application
---
## File Locations
- **Frontend files:** `/Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/frontend/`
- **Backend files:** `/Users/daveporter/Desktop/CODING-2024/Loreal-FIle-Reduction/backend/`
- **Config:** `frontend/config.js` (change API endpoint here if needed)
---
## Current Status
✅ Python backend is ready (activate venv and run app.py)
✅ All platform configurations loaded (21 total)
✅ FFmpeg is installed and detected
✅ Ready to accept requests from MAMP frontend
✅ New features: Mute controls, video specs, aspect ratio warnings
**Next:** Configure MAMP and open http://localhost:8888/index.html
---
## 🎯 Usage Flow
1. **Upload** - Drag & drop video file
2. **Detect** - Filename auto-detection works (or manual selection)
3. **Warn** - Yellow warning if aspect ratio will change
4. **Convert** - Click button, watch progress bar
5. **Compare** - See both videos side-by-side with full specs
6. **Alert** - Red warning and outline if aspect ratio was changed
7. **Listen** - Use mute controls to compare audio
8. **Download** - Get optimized file
---
## 💡 Tips for MAMP Users
1. **Always start backend first** before opening MAMP
2. **Check terminal** for backend logs and errors
3. **Use browser console (F12)** to debug frontend issues
4. **Verify document root** is set to `frontend` folder
5. **Port 5000** must be available for backend
---
## 📚 Need More Help?
- **MAMP_SETUP.md** - Detailed MAMP setup and troubleshooting
- **README.md** - Complete application documentation
- **TESTING.md** - Testing procedures
---
**Repository:** https://bitbucket.org/zlalani/loreal-video-optimizer

272
TESTING.md Normal file
View file

@ -0,0 +1,272 @@
# Testing Guide
## Manual Testing Checklist
### ✅ Backend Testing
#### 1. Health Check
```bash
curl http://localhost:5000/api/health
```
Expected response:
```json
{
"status": "ok",
"ffmpeg_installed": true,
"timestamp": "2025-..."
}
```
#### 2. Platform List
```bash
curl http://localhost:5000/api/platforms
```
Should return all 8 platforms with 21 total format combinations.
#### 3. Filename Detection
```bash
curl -X POST http://localhost:5000/api/detect \
-H "Content-Type: application/json" \
-d '{"filename": "summer_ad_tiktok_9x16.mp4"}'
```
Expected:
```json
{
"platform": "tiktok",
"aspect_ratio": "9:16",
"detected": true
}
```
---
### ✅ Frontend Testing
#### Test Cases
**1. File Upload**
- [ ] Drag & drop works
- [ ] Click to browse works
- [ ] Invalid file types rejected
- [ ] File info displays correctly
**2. Auto-Detection**
Test these filenames:
- [ ] `test_tiktok_9x16.mp4` → TikTok, 9:16
- [ ] `demo_meta_1x1.mov` → Meta, 1:1
- [ ] `ad_youtube_16x9.mp4` → YouTube, 16:9
- [ ] `campaign_pinterest_2x3.mp4` → Pinterest, 2:3
**3. Platform Selection**
- [ ] All 8 platforms listed
- [ ] Selecting platform populates aspect ratios
- [ ] Format info updates correctly
- [ ] Bitrate hints show recommended values
**4. Video Conversion**
- [ ] Convert button enables when valid
- [ ] Progress bar displays
- [ ] Conversion completes successfully
- [ ] Error handling works for invalid inputs
**5. Video Comparison**
- [ ] Both videos display
- [ ] File size stats correct
- [ ] Reduction percentage calculated
- [ ] Sync playback works
- [ ] Pause both works
- [ ] Download buttons work
**6. Edge Cases**
- [ ] Very large files (>100MB)
- [ ] Very short videos (<5 seconds)
- [ ] Videos with no audio
- [ ] Custom bitrate override
- [ ] Multiple conversions in sequence
---
### ✅ Codec Validation
Verify correct codec usage for each platform:
**TikTok (H265):**
```bash
ffprobe output_file.mp4 2>&1 | grep -i hevc
# Should contain "hevc" or "h265"
```
**YouTube (VP9):**
```bash
ffprobe output_file.webm 2>&1 | grep -i vp9
# Should contain "vp9"
```
**Meta/Others (H264):**
```bash
ffprobe output_file.mp4 2>&1 | grep -i h264
# Should contain "h264"
```
---
### ✅ Performance Testing
**Expected Conversion Times (1 minute 1080p video):**
| Platform | Expected Time | Notes |
|----------|---------------|-------|
| Meta (H264) | 20-40s | Fast |
| TikTok (H265) | 40-80s | Slower (better compression) |
| YouTube (VP9) | 60-120s | Slowest (best quality/size) |
*Times vary based on hardware and video complexity*
---
### ✅ Quality Assurance
**Visual Comparison Checks:**
1. **Sharpness** - Text remains readable
2. **Color** - No significant color shift
3. **Motion** - No excessive blocking in fast scenes
4. **Audio** - Sync and quality maintained
**Acceptable Degradation:**
- Slight softness in small details
- Minor compression artifacts in complex scenes
- Should NOT have: blocking, color banding, desync
---
### ✅ File Size Validation
**Target Reductions (vs. unoptimized source):**
| Platform | Target Reduction | Acceptable Range |
|----------|-----------------|------------------|
| TikTok | 30-40% | 20-50% |
| Meta | 25-35% | 15-45% |
| YouTube | 20-30% | 10-40% |
| Pinterest | 25-35% | 15-45% |
**Warning Signs:**
- ❌ Less than 10% reduction → Check settings
- ❌ More than 60% reduction → Quality likely too low
---
## Integration Testing
### Full Workflow Test
1. Start application: `./start.sh`
2. Upload: `sample_video_tiktok_9x16.mp4`
3. Verify auto-detection: TikTok + 9:16
4. Convert with recommended settings
5. Compare videos side-by-side
6. Verify file size reduction (target: 30-40%)
7. Download optimized file
8. Verify downloaded file plays correctly
9. Upload new file (test cleanup)
---
## Browser Compatibility
Test in:
- [ ] Chrome/Edge (Chromium)
- [ ] Firefox
- [ ] Safari (macOS/iOS)
- [ ] Mobile browsers
**Known Issues:**
- VP9/WebM may have limited support in Safari (falls back to H264)
---
## API Load Testing
### Simple Load Test
```bash
# Upload 10 files concurrently
for i in {1..10}; do
curl -X POST http://localhost:5000/api/upload \
-F "file=@test_video.mp4" &
done
wait
```
**Expected:** All uploads succeed, no crashes
---
## Error Scenarios
Test these error conditions:
1. **No FFmpeg installed**
- Remove FFmpeg temporarily
- Start server → Should warn but not crash
2. **Corrupted video file**
- Upload corrupted file → Should show error
3. **Invalid platform/aspect ratio combo**
- Try TikTok 2:3 → Should error gracefully
4. **Network interruption**
- Stop backend mid-conversion → Frontend should handle
5. **Disk space full**
- Fill disk → Should error gracefully
---
## Clean Up After Testing
```bash
# Remove test files
rm -rf backend/uploads/*
rm -rf backend/outputs/*
# Keep .gitkeep files
touch backend/uploads/.gitkeep
touch backend/outputs/.gitkeep
```
---
## Regression Testing Checklist
Before major updates, verify:
- [ ] All 21 platform/format combinations work
- [ ] Filename auto-detection patterns work
- [ ] Side-by-side comparison works
- [ ] File downloads work
- [ ] Cleanup endpoint works
- [ ] No memory leaks (long-running test)
---
## Test Video Sources
**Where to get test videos:**
1. **Pixabay** - https://pixabay.com/videos/ (Free)
2. **Pexels** - https://www.pexels.com/videos/ (Free)
3. **Generate test patterns:**
```bash
# Create 10-second test video
ffmpeg -f lavfi -i testsrc=duration=10:size=1920x1080:rate=30 \
-pix_fmt yuv420p test_pattern.mp4
```
---
**Testing complete!** All features verified and working as expected.

View file

@ -1,133 +0,0 @@
# ==============================================================================
# VIDEO OPTIMIZER - BACKEND SECURITY CONFIGURATION
# ==============================================================================
# Location: /opt/video-optimizer-back/backend/.htaccess
# Purpose: Deny all direct web access to backend files
# Note: Backend should ONLY be accessed via Apache proxy (localhost:5000)
# ==============================================================================
# ------------------------------------------------------------------------------
# DENY ALL ACCESS
# ------------------------------------------------------------------------------
# This backend directory should NOT be directly accessible via web
# All API requests should go through Apache proxy: /video-optimizer/api -> localhost:5000
<RequireAll>
Require all denied
</RequireAll>
# ------------------------------------------------------------------------------
# EXPLANATION
# ------------------------------------------------------------------------------
#
# The backend Python Flask application runs on localhost:5000 and should ONLY
# be accessible through the Apache reverse proxy configuration.
#
# Direct web access to this directory must be blocked to prevent:
# - Direct access to Python source code
# - Exposure of sensitive configuration files
# - Unauthorized API access bypassing the proxy
# - Security vulnerabilities from direct file access
#
# Correct API access path:
# ✓ https://ai-sandbox.oliver.solutions/video-optimizer/api/health
# ✗ Direct access to /opt/video-optimizer-back/backend/app.py
#
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# ADDITIONAL PROTECTION
# ------------------------------------------------------------------------------
# Disable directory browsing
Options -Indexes
# Disable symbolic links
Options -FollowSymLinks
# Disable script execution
Options -ExecCGI
# Deny access to all file types
<FilesMatch ".*">
Require all denied
</FilesMatch>
# Explicitly deny Python files
<FilesMatch "\.(py|pyc|pyo|pyd)$">
Require all denied
</FilesMatch>
# Deny access to environment files
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
# Deny access to JSON configuration files
<FilesMatch "\.(json)$">
Require all denied
</FilesMatch>
# Deny access to log files
<FilesMatch "\.(log)$">
Require all denied
</FilesMatch>
# Deny access to requirements.txt
<FilesMatch "^requirements\.txt$">
Require all denied
</FilesMatch>
# Deny access to .htaccess itself
<Files ".htaccess">
Require all denied
</Files>
# Deny access to hidden files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# ------------------------------------------------------------------------------
# SECURITY HEADERS (In case of misconfiguration)
# ------------------------------------------------------------------------------
<IfModule mod_headers.c>
# If somehow accessed, prevent rendering in browser
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set X-XSS-Protection "1; mode=block"
# Prevent caching
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header set Pragma "no-cache"
Header set Expires "0"
</IfModule>
# ==============================================================================
# IMPORTANT NOTES
# ==============================================================================
#
# 1. This directory (/opt/video-optimizer-back/backend/) is NOT in the web root
# (/var/www/html/), so it should not be accessible via Apache anyway.
#
# 2. This .htaccess file is a defense-in-depth measure to prevent access
# in case of Apache misconfiguration.
#
# 3. The backend Flask application is bound to 127.0.0.1:5000 (localhost only)
# and cannot be accessed directly from the internet.
#
# 4. All API requests must go through the Apache proxy configuration:
# <Location /video-optimizer/api>
# ProxyPass http://127.0.0.1:5000/api
# ProxyPassReverse http://127.0.0.1:5000/api
# </Location>
#
# 5. If you need to access backend files for maintenance, use SSH:
# ssh user@ai-sandbox.oliver.solutions
# cd /opt/video-optimizer-back/backend/
#
# ==============================================================================
# END OF CONFIGURATION
# ==============================================================================

View file

@ -1,19 +1,15 @@
"""
Flask backend for video optimization tool.
Includes Box.com automation (webhook + polling) on the same port.
Flask backend for video optimization tool
"""
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import os
import uuid
import json
import time
from datetime import datetime
from video_processor import VideoProcessor
from conversion_logger import ConversionLogger
from platform_specs import (
PLATFORM_SPECS,
FILENAME_PATTERNS,
@ -25,34 +21,8 @@ from platform_specs import (
get_platform_info
)
# Box integration (optional — gracefully disabled if boxsdk not installed)
BOX_AVAILABLE = False
try:
from box_processor import BoxProcessor, BoxPoller, verify_box_signature
BOX_AVAILABLE = True
except ImportError:
print("Box SDK not installed — Box automation disabled. Run: pip install boxsdk[jwt]")
# Load environment variables from .env file
load_dotenv()
app = Flask(__name__)
# ==============================================================================
# ENVIRONMENT-AWARE CONFIGURATION
# ==============================================================================
# Read environment setting (development or production)
FLASK_ENV = os.getenv('FLASK_ENV', 'development')
IS_PRODUCTION = FLASK_ENV == 'production'
# Configure CORS based on environment
if IS_PRODUCTION:
# Production: Restrict CORS to frontend URL only
FRONTEND_URL = os.getenv('FRONTEND_URL', 'https://ai-sandbox.oliver.solutions/video-optimizer')
CORS(app, origins=[FRONTEND_URL])
else:
# Development: Allow all origins for easier testing
CORS(app)
CORS(app)
# Store factory defaults (original specs from platform_specs.py)
import copy
@ -60,61 +30,19 @@ FACTORY_DEFAULTS = copy.deepcopy(PLATFORM_SPECS)
FACTORY_FILENAME_PATTERNS = copy.deepcopy(FILENAME_PATTERNS)
FACTORY_ASPECT_RATIO_PATTERNS = copy.deepcopy(ASPECT_RATIO_PATTERNS)
# ==============================================================================
# FILE AND FOLDER CONFIGURATION
# ==============================================================================
# Configuration
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')
OUTPUT_FOLDER = os.path.join(os.path.dirname(__file__), 'outputs')
LOGS_FOLDER = os.path.join(os.path.dirname(__file__), 'logs')
CONVERSION_LOGS_FOLDER = os.path.join(LOGS_FOLDER, 'conversions')
ALLOWED_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v'}
# Read max file size from environment (in MB), convert to bytes
MAX_FILE_SIZE_MB = int(os.getenv('MAX_FILE_SIZE_MB', '500'))
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024
# Read file retention settings
FILE_RETENTION_HOURS = int(os.getenv('FILE_RETENTION_HOURS', '24'))
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
# Create folders if they don't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
os.makedirs(LOGS_FOLDER, exist_ok=True)
os.makedirs(CONVERSION_LOGS_FOLDER, exist_ok=True)
# ==============================================================================
# FLASK APPLICATION CONFIGURATION
# ==============================================================================
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
# Read backend port from environment
BACKEND_PORT = int(os.getenv('BACKEND_PORT', '5000'))
# ==============================================================================
# BOX AUTOMATION CONFIGURATION
# ==============================================================================
BOX_CONFIG_PATH = os.path.abspath(os.getenv(
'BOX_CONFIG_PATH',
os.path.join(os.path.dirname(__file__), '..', 'oliver_box_config.json')
))
BOX_VIDEO_OPTIMIZER_FOLDER_ID = os.getenv('BOX_VIDEO_OPTIMIZER_FOLDER_ID', '')
BOX_AS_USER_ID = os.getenv('BOX_AS_USER_ID', '')
BOX_WEBHOOK_SECRET = os.getenv('BOX_WEBHOOK_SECRET', '')
BOX_USE_POLLING = os.getenv('BOX_USE_POLLING', 'false').lower() == 'true'
BOX_POLL_INTERVAL_SECONDS = int(os.getenv('BOX_POLL_INTERVAL_SECONDS', '60'))
# Global Box instances — set by init_box() at startup
box_processor = None
box_poller = None
# ==============================================================================
# INITIALIZE CONVERSION LOGGER
# ==============================================================================
conversion_logger = ConversionLogger(CONVERSION_LOGS_FOLDER)
def allowed_file(filename):
@ -133,16 +61,6 @@ def health_check():
})
@app.route('/api/config', methods=['GET'])
def get_config():
"""Get Azure AD configuration for Microsoft SSO"""
return jsonify({
'AZURE_CLIENT_ID': os.getenv('AZURE_CLIENT_ID'),
'AZURE_TENANT_ID': os.getenv('AZURE_TENANT_ID'),
'REDIRECT_URI': os.getenv('REDIRECT_URI', 'http://localhost:3000')
})
@app.route('/api/platforms', methods=['GET'])
def get_platforms():
"""Get all available platforms and their specifications"""
@ -233,7 +151,6 @@ def convert_video():
platform = data.get('platform')
aspect_ratio = data.get('aspect_ratio')
custom_bitrate = data.get('custom_bitrate')
user_email = data.get('user_email', 'anonymous@local') # Get user email from request
if not all([file_id, platform, aspect_ratio]):
return jsonify({'error': 'Missing required parameters'}), 400
@ -247,9 +164,6 @@ def convert_video():
input_path = os.path.join(app.config['UPLOAD_FOLDER'], input_files[0])
# Start timing the conversion
start_time = time.time()
try:
# Get platform info to determine output container
platform_info = get_platform_info(platform)
@ -260,9 +174,6 @@ def convert_video():
output_filename = f"{file_id}_optimized.{output_extension}"
output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename)
# Get input file size before conversion
input_size = os.path.getsize(input_path)
# Process video
processor = VideoProcessor(input_path)
result = processor.convert_video(
@ -272,26 +183,11 @@ def convert_video():
custom_bitrate=custom_bitrate
)
# Calculate conversion duration
conversion_duration = time.time() - start_time
# Calculate size reduction
input_size = os.path.getsize(input_path)
output_size = result['output_size']
size_reduction = ((input_size - output_size) / input_size) * 100
# Log successful conversion
conversion_logger.log_conversion(
user_email=user_email,
platform=platform,
aspect_ratio=aspect_ratio,
input_file_size=input_size,
output_file_size=output_size,
conversion_duration=conversion_duration,
status='success',
file_id=file_id,
error_message=None
)
return jsonify({
'success': True,
'output_file_id': file_id,
@ -303,28 +199,6 @@ def convert_video():
})
except Exception as e:
# Calculate conversion duration (even for failures)
conversion_duration = time.time() - start_time
# Get file sizes (use 0 for output if conversion failed)
try:
input_size = os.path.getsize(input_path)
except:
input_size = 0
# Log failed conversion
conversion_logger.log_conversion(
user_email=user_email,
platform=platform,
aspect_ratio=aspect_ratio,
input_file_size=input_size,
output_file_size=0,
conversion_duration=conversion_duration,
status='failure',
file_id=file_id,
error_message=str(e)
)
return jsonify({'error': str(e)}), 500
@ -653,148 +527,6 @@ def admin_save_naming_conventions():
return jsonify({'error': str(e)}), 500
# ==============================================================================
# BOX AUTOMATION ENDPOINTS
# ==============================================================================
@app.route('/api/admin/box-history', methods=['GET'])
def admin_box_history():
"""
Box automation processing history from conversion logs.
?date=YYYY-MM-DD single specific date (defaults to today)
?days=7 aggregate last N days, max 30 (overrides date)
"""
days_param = request.args.get('days')
date_param = request.args.get('date')
if days_param:
from datetime import timedelta
n = min(int(days_param), 30)
all_jobs = []
today = datetime.now()
for i in range(n):
day = (today - timedelta(days=i)).strftime('%Y-%m-%d')
all_jobs.extend(conversion_logger.get_user_logs('box_automation@system', day))
all_jobs.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
return jsonify({
'success': True,
'days': n,
'jobs': all_jobs,
'total': len(all_jobs)
})
else:
date = date_param or datetime.now().strftime('%Y-%m-%d')
jobs = conversion_logger.get_user_logs('box_automation@system', date)
jobs_sorted = sorted(jobs, key=lambda x: x.get('timestamp', ''), reverse=True)
return jsonify({
'success': True,
'date': date,
'jobs': jobs_sorted,
'total': len(jobs_sorted)
})
@app.route('/webhooks/box', methods=['POST'])
def box_webhook():
"""Receive Box webhook events (FILE.UPLOADED / FILE.COPIED on IN folder)"""
if not BOX_AVAILABLE:
return jsonify({'error': 'Box SDK not installed'}), 503
if not box_processor:
return jsonify({'error': 'Box automation not initialised — check BOX_VIDEO_OPTIMIZER_FOLDER_ID in .env'}), 503
body = request.get_data()
primary_sig = request.headers.get('Box-Signature-Primary', '')
secondary_sig = request.headers.get('Box-Signature-Secondary', '')
if not verify_box_signature(BOX_WEBHOOK_SECRET, body, primary_sig, secondary_sig):
print("✗ Box webhook: invalid signature")
return jsonify({'error': 'Invalid signature'}), 401
try:
payload = request.get_json()
event_type = payload.get('trigger')
source = payload.get('source', {})
file_id = source.get('id')
filename = source.get('name')
print(f"\n📥 Box webhook: {event_type}{filename} (ID: {file_id})")
if event_type not in ['FILE.UPLOADED', 'FILE.COPIED']:
return jsonify({'message': 'Event ignored'}), 200
result = box_processor.process_file(file_id, filename)
return jsonify(result), 200
except Exception as e:
print(f"✗ Box webhook error: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/box/trigger', methods=['POST'])
def box_manual_trigger():
"""Manually trigger Box processing — for testing without a live webhook.
Body: { "file_id": "...", "filename": "campaign_tiktok_9x16.mp4" }
"""
if not BOX_AVAILABLE:
return jsonify({'error': 'Box SDK not installed'}), 503
if not box_processor:
return jsonify({'error': 'Box automation not initialised — check BOX_VIDEO_OPTIMIZER_FOLDER_ID in .env'}), 503
data = request.get_json()
file_id = data.get('file_id')
filename = data.get('filename')
if not file_id or not filename:
return jsonify({'error': 'Missing file_id or filename'}), 400
print(f"\n🔧 Box manual trigger: {filename} (ID: {file_id})")
result = box_processor.process_file(file_id, filename)
return jsonify(result), 200
@app.route('/api/box/health', methods=['GET'])
def box_health():
"""Box automation health — shows folder config and polling status"""
return jsonify({
'box_available': BOX_AVAILABLE,
'box_initialised': box_processor is not None,
'folders_configured': len(box_processor.folders) == 3 if box_processor else False,
'folders': box_processor.folders if box_processor else {},
'polling_enabled': BOX_USE_POLLING,
'polling_interval_seconds': BOX_POLL_INTERVAL_SECONDS if BOX_USE_POLLING else None,
'timestamp': datetime.now().isoformat()
}), 200
# ==============================================================================
# BOX INITIALISATION
# ==============================================================================
def init_box():
"""Initialise Box processor at startup. Non-fatal — web UI works without it."""
global box_processor, box_poller
if not BOX_AVAILABLE:
print("Box automation: SDK not installed — skipping")
return
if not BOX_VIDEO_OPTIMIZER_FOLDER_ID:
print("Box automation: BOX_VIDEO_OPTIMIZER_FOLDER_ID not set — skipping")
return
try:
bp = BoxProcessor(BOX_CONFIG_PATH, CONVERSION_LOGS_FOLDER, BOX_AS_USER_ID)
if bp.initialize(BOX_VIDEO_OPTIMIZER_FOLDER_ID):
box_processor = bp
if BOX_USE_POLLING:
box_poller = BoxPoller(box_processor, BOX_POLL_INTERVAL_SECONDS)
box_poller.start()
else:
print("Box automation: initialisation failed — Box features disabled")
except Exception as e:
print(f"Box automation: error during initialisation: {e} — Box features disabled")
if __name__ == '__main__':
# Load specs from file if exists
saved_specs = load_specs_from_file()
@ -817,36 +549,8 @@ if __name__ == '__main__':
print("WARNING: FFmpeg not found. Please install FFmpeg to use video conversion features.")
print("Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)")
# Initialise Box automation (non-fatal)
init_box()
print("=" * 80)
print("VIDEO OPTIMIZER - STARTING SERVER")
print("=" * 80)
print(f"Environment: {FLASK_ENV}")
print(f"Debug mode: {not IS_PRODUCTION}")
print(f"Backend port: {BACKEND_PORT}")
print("Starting Video Optimization Server...")
print(f"Upload folder: {UPLOAD_FOLDER}")
print(f"Output folder: {OUTPUT_FOLDER}")
print(f"Logs folder: {LOGS_FOLDER}")
print(f"Max file size: {MAX_FILE_SIZE_MB}MB")
print(f"File retention: {FILE_RETENTION_HOURS} hours")
print(f"Platforms configured: {len(PLATFORM_SPECS)}")
if IS_PRODUCTION:
print(f"CORS restricted to: {FRONTEND_URL}")
else:
print("CORS: All origins allowed (development mode)")
print(f"Box automation: {'enabled' if box_processor else 'disabled'}")
if box_processor:
mode = f"polling every {BOX_POLL_INTERVAL_SECONDS}s" if BOX_USE_POLLING else "webhook mode"
print(f"Box mode: {mode}")
print(f"Box webhook URL: POST http://localhost:{BACKEND_PORT}/webhooks/box")
print("=" * 80)
# threaded=True so Box conversions in background don't block web API requests
app.run(
debug=(not IS_PRODUCTION),
host='0.0.0.0',
port=BACKEND_PORT,
threaded=True
)
print(f"Platforms: {len(PLATFORM_SPECS)} configured")
app.run(debug=True, host='0.0.0.0', port=5000)

View file

@ -1,262 +0,0 @@
"""
Box.com SDK wrapper for JWT authentication and folder operations
Handles authentication, folder discovery, file download/upload
"""
from boxsdk import JWTAuth, Client
from boxsdk.exception import BoxException
import json
import os
from typing import Optional, Dict, List
class BoxClient:
"""Wrapper for Box SDK with JWT authentication"""
def __init__(self, config_path: str, as_user_id: str = ''):
"""
Initialize Box client with JWT authentication
Args:
config_path: Path to oliver_box_config.json
as_user_id: Optional Box user ID to impersonate (BOX_AS_USER_ID in .env)
"""
self.config_path = config_path
self.as_user_id = as_user_id
self.client = None
self.folder_cache = {}
def authenticate(self) -> bool:
"""
Authenticate using JWT credentials, optionally impersonating an enterprise user.
Returns:
bool: True if authentication successful
"""
try:
with open(self.config_path, 'r') as f:
config = json.load(f)
auth = JWTAuth.from_settings_dictionary(config)
service_client = Client(auth)
if self.as_user_id:
# Impersonate the enterprise user who owns the VIDEO_OPTIMIZER folder
user = service_client.user(self.as_user_id)
self.client = service_client.as_user(user)
user_info = self.client.user().get()
print(f"✓ Authenticated as enterprise user: {user_info.name} ({user_info.login})")
else:
self.client = service_client
user_info = self.client.user().get()
print(f"✓ Authenticated as service account: {user_info.name} ({user_info.login})")
return True
except FileNotFoundError:
print(f"✗ Box config file not found: {self.config_path}")
return False
except Exception as e:
print(f"✗ Box authentication failed: {str(e)}")
return False
def list_enterprise_users(self) -> List[Dict]:
"""
List enterprise users to help find the correct BOX_AS_USER_ID.
Run this once to identify the user who owns the VIDEO_OPTIMIZER folder.
"""
try:
print("Enterprise users:")
users = []
for user in self.client.users():
users.append({'id': user.id, 'name': user.name, 'login': user.login})
print(f" ID: {user.id} | {user.name} ({user.login})")
return users
except BoxException as e:
print(f"✗ Error listing users: {str(e)}")
return []
def discover_folders(self, video_optimizer_folder_id: str) -> Dict[str, str]:
"""
Locate IN, OUT_SUCCESS, OUT_FAILED directly inside the VIDEO_OPTIMIZER folder.
Args:
video_optimizer_folder_id: Box folder ID for VIDEO_OPTIMIZER (from env)
Returns:
Dict mapping folder names to folder IDs
e.g., {'IN': '123456', 'OUT_SUCCESS': '789012', 'OUT_FAILED': '345678'}
"""
folders = {}
target_names = ['IN', 'OUT_SUCCESS', 'OUT_FAILED']
try:
print(f"Looking for subfolders inside VIDEO_OPTIMIZER (ID: {video_optimizer_folder_id})...")
items = self.client.folder(video_optimizer_folder_id).get_items()
for item in items:
if item.type == 'folder' and item.name in target_names:
folders[item.name] = item.id
print(f"✓ Found {item.name} (ID: {item.id})")
except BoxException as e:
print(f"✗ Error accessing VIDEO_OPTIMIZER folder: {str(e)}")
print(f" Make sure the service account has been shared on the VIDEO_OPTIMIZER folder.")
self.folder_cache = folders
missing = [n for n in target_names if n not in folders]
if missing:
print(f"✗ Missing required folders inside VIDEO_OPTIMIZER: {missing}")
return folders
def download_file(self, file_id: str, destination_path: str) -> bool:
"""
Download file from Box to local path
Args:
file_id: Box file ID
destination_path: Local file path to save to
Returns:
bool: True if download successful
"""
try:
# Ensure destination directory exists
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
with open(destination_path, 'wb') as f:
self.client.file(file_id).download_to(f)
file_size = os.path.getsize(destination_path)
print(f"✓ Downloaded file {file_id} ({file_size} bytes)")
return True
except BoxException as e:
print(f"✗ Error downloading file {file_id}: {str(e)}")
return False
except Exception as e:
print(f"✗ Unexpected error downloading file {file_id}: {str(e)}")
return False
def upload_file(self, folder_id: str, file_path: str, file_name: Optional[str] = None) -> Optional[str]:
"""
Upload file to Box folder
Args:
folder_id: Target Box folder ID
file_path: Local file path to upload
file_name: Optional custom filename (defaults to original)
Returns:
str: Uploaded file ID, or None if failed
"""
try:
if not os.path.exists(file_path):
print(f"✗ File not found: {file_path}")
return None
if file_name is None:
file_name = os.path.basename(file_path)
folder = self.client.folder(folder_id)
uploaded_file = folder.upload(file_path, file_name)
file_size = os.path.getsize(file_path)
print(f"✓ Uploaded {file_name} to folder {folder_id} ({file_size} bytes)")
return uploaded_file.id
except BoxException as e:
print(f"✗ Error uploading {file_path} to folder {folder_id}: {str(e)}")
return None
except Exception as e:
print(f"✗ Unexpected error uploading {file_path}: {str(e)}")
return None
def get_file_info(self, file_id: str) -> Optional[Dict]:
"""
Get file metadata from Box
Args:
file_id: Box file ID
Returns:
Dict with file info (name, size, modified_at, etc.)
"""
try:
file = self.client.file(file_id).get()
return {
'id': file.id,
'name': file.name,
'size': file.size,
'modified_at': file.modified_at,
'created_at': file.created_at
}
except BoxException as e:
print(f"✗ Error getting file info for {file_id}: {str(e)}")
return None
except Exception as e:
print(f"✗ Unexpected error getting file info for {file_id}: {str(e)}")
return None
def download_with_retry(self, file_id: str, destination_path: str, max_retries: int = 3) -> bool:
"""
Download file with exponential backoff retry
Args:
file_id: Box file ID
destination_path: Local file path to save to
max_retries: Maximum number of retry attempts
Returns:
bool: True if download successful
"""
import time
for attempt in range(max_retries):
try:
if self.download_file(file_id, destination_path):
return True
except Exception as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # 1s, 2s, 4s
print(f"⚠ Download attempt {attempt + 1} failed, retrying in {wait_time}s...")
time.sleep(wait_time)
else:
print(f"✗ Download failed after {max_retries} attempts")
raise e
return False
def upload_with_retry(self, folder_id: str, file_path: str, file_name: Optional[str] = None,
max_retries: int = 3) -> Optional[str]:
"""
Upload file with exponential backoff retry
Args:
folder_id: Target Box folder ID
file_path: Local file path to upload
file_name: Optional custom filename
max_retries: Maximum number of retry attempts
Returns:
str: Uploaded file ID, or None if failed
"""
import time
for attempt in range(max_retries):
try:
file_id = self.upload_file(folder_id, file_path, file_name)
if file_id:
return file_id
except Exception as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # 1s, 2s, 4s
print(f"⚠ Upload attempt {attempt + 1} failed, retrying in {wait_time}s...")
time.sleep(wait_time)
else:
print(f"✗ Upload failed after {max_retries} attempts")
raise e
return None

View file

@ -1,426 +0,0 @@
"""
Box.com video processing classes.
BoxProcessor and BoxPoller are imported and used directly by app.py.
This module no longer runs as a standalone service all Box endpoints
live in app.py on port 5000.
To run the Box automation:
python app.py # starts web UI + Box in one process
To test:
python test_box_processor.py # targets http://localhost:5000
python box_setup.py # verifies Box credentials and folders
"""
import os
import json
import time
import hmac
import hashlib
import tempfile
import shutil
import threading
from datetime import datetime
from typing import Optional, Dict, Tuple, Set
from box_client import BoxClient
from video_processor import VideoProcessor
from conversion_logger import ConversionLogger
from platform_specs import (
detect_platform_from_filename,
detect_aspect_ratio_from_filename,
get_platform_info
)
def verify_box_signature(secret: str, body: bytes, primary: str, secondary: str = '') -> bool:
"""
Verify Box webhook HMAC-SHA256 signature.
Box sends both Box-Signature-Primary and Box-Signature-Secondary headers.
A valid signature on either is sufficient.
Returns True (skip verification) if secret is empty.
"""
if not secret:
return True
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
if primary and hmac.compare_digest(expected, primary):
return True
if secondary and hmac.compare_digest(expected, secondary):
return True
return False
class BoxProcessor:
"""Main Box video processor — 8-step pipeline per file"""
def __init__(self, box_config_path: str, conversion_logs_folder: str, as_user_id: str = ''):
self.box_client = BoxClient(box_config_path, as_user_id)
self.conversion_logger = ConversionLogger(conversion_logs_folder)
self.folders = {}
def initialize(self, video_optimizer_folder_id: str) -> bool:
"""Authenticate with Box and discover IN / OUT_SUCCESS / OUT_FAILED folders"""
print("=" * 70)
print("BOX AUTOMATION - INITIALIZING")
print("=" * 70)
if not video_optimizer_folder_id:
print("✗ BOX_VIDEO_OPTIMIZER_FOLDER_ID not set in .env")
return False
if not self.box_client.authenticate():
return False
self.folders = self.box_client.discover_folders(video_optimizer_folder_id)
required = ['IN', 'OUT_SUCCESS', 'OUT_FAILED']
missing = [f for f in required if f not in self.folders]
if missing:
print(f"✗ Missing required Box folders: {missing}")
return False
print("=" * 70)
print("✓ Box automation ready")
print(f" IN folder: {self.folders['IN']}")
print(f" OUT_SUCCESS folder: {self.folders['OUT_SUCCESS']}")
print(f" OUT_FAILED folder: {self.folders['OUT_FAILED']}")
print("=" * 70)
return True
def validate_filename(self, filename: str) -> Tuple[bool, Optional[str], Optional[str], Optional[str]]:
"""
Validate filename contains detectable platform and aspect ratio.
Returns (valid, platform, aspect_ratio, error_message)
"""
platform = detect_platform_from_filename(filename)
aspect_ratio = detect_aspect_ratio_from_filename(filename)
if platform is None:
return (False, None, None,
"No platform detected. Expected patterns like _tiktok_, _meta_, _yt_, etc.")
if aspect_ratio is None:
return (False, platform, None,
"No aspect ratio detected. Expected patterns like _9x16_, _16x9_, _1x1_, etc.")
platform_info = get_platform_info(platform)
if not platform_info:
return (False, platform, aspect_ratio, f"Invalid platform: {platform}")
supported_ratios = [fmt['ratio'] for fmt in platform_info['formats']]
if aspect_ratio not in supported_ratios:
return (False, platform, aspect_ratio,
f"Platform '{platform}' does not support {aspect_ratio}. "
f"Supported: {', '.join(supported_ratios)}")
return (True, platform, aspect_ratio, None)
def process_file(self, file_id: str, filename: str) -> Dict:
"""
Main 8-step processing pipeline:
1. Validate filename
2. Download from Box IN
3. Convert with FFmpeg
4. Generate JSON report
5. Upload video to OUT_SUCCESS
6. Upload report to OUT_SUCCESS
7. Delete original from IN
8. Log + cleanup temp files
"""
job_id = f"{int(time.time())}_{file_id}"
start_time = time.time()
temp_dir = os.path.join(tempfile.gettempdir(), 'box_processor', job_id)
os.makedirs(temp_dir, exist_ok=True)
result = {
'job_id': job_id,
'file_id': file_id,
'filename': filename,
'status': 'pending',
'timestamp': datetime.now().isoformat()
}
print(f"\n{'=' * 70}")
print(f"BOX: Processing {filename}")
print(f"Job: {job_id}")
print(f"{'=' * 70}")
try:
# 1. Validate filename
print("\n[1/8] Validating filename...")
valid, platform, aspect_ratio, error = self.validate_filename(filename)
if not valid:
print(f"✗ Validation failed: {error}")
result.update({'status': 'skipped', 'error': error,
'platform': platform, 'aspect_ratio': aspect_ratio})
report = self.generate_error_report(filename, error, platform, aspect_ratio)
self.upload_error_report(filename, report)
return result
print(f"✓ Platform: {platform} | Aspect ratio: {aspect_ratio}")
result['platform'] = platform
result['aspect_ratio'] = aspect_ratio
# 2. Download from Box
print("\n[2/8] Downloading from Box IN folder...")
input_path = os.path.join(temp_dir, 'input_' + filename)
if not self.box_client.download_with_retry(file_id, input_path):
raise Exception("Failed to download file from Box")
input_size = os.path.getsize(input_path)
result['input_size'] = input_size
print(f"✓ Downloaded ({input_size / (1024 * 1024):.2f} MB)")
# Probe original video for report metadata
original_info = {}
try:
original_info = VideoProcessor(input_path).get_video_info() or {}
except Exception as e:
print(f"⚠ Could not probe original metadata: {e}")
# 3. Convert
print("\n[3/8] Converting with FFmpeg...")
platform_info = get_platform_info(platform)
container = platform_info['container']
output_filename = f"{os.path.splitext(filename)[0]}_optimized.{container}"
output_path = os.path.join(temp_dir, output_filename)
conversion_result = VideoProcessor(input_path).convert_video(
platform=platform,
aspect_ratio=aspect_ratio,
output_path=output_path,
custom_bitrate=None
)
output_size = os.path.getsize(output_path)
size_reduction = ((input_size - output_size) / input_size) * 100
result['output_size'] = output_size
result['size_reduction_percent'] = round(size_reduction, 2)
result['conversion_details'] = conversion_result
print(f"✓ Done ({output_size / (1024 * 1024):.2f} MB, {size_reduction:.1f}% reduction)")
# 4. Generate report
print("\n[4/8] Generating JSON report...")
report = self.generate_success_report(
filename=filename,
output_filename=output_filename,
platform=platform,
aspect_ratio=aspect_ratio,
input_size=input_size,
output_size=output_size,
original_info=original_info,
conversion_result=conversion_result,
duration=time.time() - start_time
)
print("✓ Report ready")
# 5 & 6. Upload video + report to OUT_SUCCESS
print("\n[5/8] Uploading video to OUT_SUCCESS...")
success_folder_id = self.folders['OUT_SUCCESS']
video_file_id = self.box_client.upload_with_retry(
success_folder_id, output_path, output_filename)
if not video_file_id:
raise Exception("Failed to upload optimised video to OUT_SUCCESS")
print("\n[6/8] Uploading report to OUT_SUCCESS...")
report_filename = f"{os.path.splitext(filename)[0]}_report.json"
report_path = os.path.join(temp_dir, report_filename)
with open(report_path, 'w') as f:
json.dump(report, f, indent=2)
report_file_id = self.box_client.upload_with_retry(
success_folder_id, report_path, report_filename)
result['status'] = 'success'
result['uploaded_video_id'] = video_file_id
result['uploaded_report_id'] = report_file_id
print("✓ Uploads complete")
# 7. Delete original from IN
print("\n[7/8] Deleting original from IN folder...")
try:
self.box_client.client.file(file_id).delete()
print(f"✓ Deleted original (ID: {file_id})")
except Exception as del_err:
print(f"⚠ Could not delete original from IN: {del_err}")
# 8. Log
print("\n[8/8] Logging conversion...")
self.conversion_logger.log_conversion(
user_email='box_automation@system',
platform=platform,
aspect_ratio=aspect_ratio,
input_file_size=input_size,
output_file_size=output_size,
conversion_duration=time.time() - start_time,
status='success',
file_id=file_id,
error_message=None
)
print("✓ Logged")
except Exception as e:
print(f"\n✗ ERROR: {e}")
result['status'] = 'failed'
result['error'] = str(e)
self.upload_error_report(filename, self.generate_error_report(
filename, str(e), result.get('platform'), result.get('aspect_ratio')
), failed=True)
self.conversion_logger.log_conversion(
user_email='box_automation@system',
platform=result.get('platform', 'unknown'),
aspect_ratio=result.get('aspect_ratio', 'unknown'),
input_file_size=result.get('input_size', 0),
output_file_size=0,
conversion_duration=time.time() - start_time,
status='failure',
file_id=file_id,
error_message=str(e)
)
finally:
try:
shutil.rmtree(temp_dir)
except Exception:
pass
print(f"\n{'=' * 70}")
print(f"BOX: {result['status'].upper()}{filename} ({time.time() - start_time:.1f}s)")
print(f"{'=' * 70}\n")
return result
def generate_success_report(self, filename: str, output_filename: str,
platform: str, aspect_ratio: str,
input_size: int, output_size: int,
original_info: Dict, conversion_result: Dict,
duration: float) -> Dict:
size_reduction = ((input_size - output_size) / input_size) * 100
return {
'status': 'success',
'timestamp': datetime.now().isoformat(),
'processing_time_seconds': round(duration, 2),
'original_file': {
'filename': filename,
'size_bytes': input_size,
'size_mb': round(input_size / (1024 * 1024), 2),
'codec': original_info.get('codec', 'unknown'),
'resolution': original_info.get('resolution', 'unknown'),
'bitrate': original_info.get('bitrate', 'unknown'),
'duration_seconds': original_info.get('duration', 0),
'aspect_ratio': original_info.get('aspect_ratio', 'unknown')
},
'optimised_file': {
'filename': output_filename,
'size_bytes': output_size,
'size_mb': round(output_size / (1024 * 1024), 2),
'size_reduction_percent': round(size_reduction, 2),
'savings_mb': round((input_size - output_size) / (1024 * 1024), 2)
},
'conversion_details': {
'platform': platform,
'aspect_ratio': aspect_ratio,
'resolution': conversion_result.get('resolution', 'N/A'),
'codec': conversion_result.get('codec', 'N/A'),
'bitrate': conversion_result.get('bitrate', 'N/A'),
'duration_seconds': conversion_result.get('duration', 0)
}
}
def generate_error_report(self, filename: str, error: str,
platform: Optional[str], aspect_ratio: Optional[str]) -> Dict:
return {
'status': 'error',
'timestamp': datetime.now().isoformat(),
'original_file': {'filename': filename},
'error': {
'message': error,
'detected_platform': platform,
'detected_aspect_ratio': aspect_ratio,
'reason': (
'Filename must include both a platform pattern (e.g. _tiktok_, _meta_) '
'and an aspect ratio pattern (e.g. _9x16_, _16x9_, _1x1_)'
)
}
}
def upload_error_report(self, filename: str, report: Dict, failed: bool = False):
folder_id = self.folders.get('OUT_FAILED')
if not folder_id:
return
temp_path = os.path.join(tempfile.gettempdir(), f"error_{int(time.time())}.json")
try:
with open(temp_path, 'w') as f:
json.dump(report, f, indent=2)
report_name = f"{os.path.splitext(filename)[0]}_error_report.json"
self.box_client.upload_with_retry(folder_id, temp_path, report_name)
print("✓ Error report uploaded to OUT_FAILED")
except Exception as e:
print(f"⚠ Failed to upload error report: {e}")
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
class BoxPoller:
"""
Polls the Box IN folder on a configurable interval.
Alternative to webhooks no public URL required.
Activated by BOX_USE_POLLING=true in .env.
Interval controlled by BOX_POLL_INTERVAL_SECONDS (default 60).
"""
def __init__(self, processor: BoxProcessor, interval_seconds: int = 60):
self.processor = processor
self.interval = interval_seconds
self._processed: Set[str] = set()
self._running = False
self._thread: Optional[threading.Thread] = None
def start(self):
self._running = True
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
print(f"✓ Box polling active — checking IN folder every {self.interval}s")
def stop(self):
self._running = False
if self._thread:
self._thread.join(timeout=5)
def _poll_loop(self):
print(f"[POLLER] Started (interval: {self.interval}s)")
while self._running:
try:
self._check_in_folder()
except Exception as e:
print(f"[POLLER] Error: {e}")
time.sleep(self.interval)
def _check_in_folder(self):
in_folder_id = self.processor.folders.get('IN')
if not in_folder_id:
return
items = self.processor.box_client.client.folder(in_folder_id).get_items()
new_files = [item for item in items
if item.type == 'file' and item.id not in self._processed]
if not new_files:
print(f"[POLLER] No new files ({datetime.now().strftime('%H:%M:%S')})")
return
print(f"[POLLER] Found {len(new_files)} new file(s)")
for item in new_files:
self._processed.add(item.id)
self.processor.process_file(item.id, item.name)

View file

@ -1,277 +0,0 @@
"""
Box.com setup & diagnostic script
Run this once to:
1. Verify JWT credentials in oliver_box_config.json
2. Authenticate as the service account
3. List enterprise users (to identify BOX_AS_USER_ID)
4. Access the VIDEO_OPTIMIZER root folder
5. Discover IN / OUT_SUCCESS / OUT_FAILED sub-folders
6. Print a summary of what is configured and what is missing
Usage:
cd backend
python box_setup.py
Optional: test with a specific Box user ID
BOX_AS_USER_ID=12345678 python box_setup.py
"""
import os
import sys
import json
from dotenv import load_dotenv
# Load .env from the parent directory
load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env'))
BOX_CONFIG_PATH = os.getenv(
'BOX_CONFIG_PATH',
os.path.join(os.path.dirname(__file__), '..', 'oliver_box_config.json')
)
BOX_VIDEO_OPTIMIZER_FOLDER_ID = os.getenv('BOX_VIDEO_OPTIMIZER_FOLDER_ID', '')
BOX_AS_USER_ID = os.getenv('BOX_AS_USER_ID', '')
def print_section(title: str):
print(f"\n{'=' * 70}")
print(f" {title}")
print('=' * 70)
def check_config_file():
print_section("1. Checking oliver_box_config.json")
resolved = os.path.abspath(BOX_CONFIG_PATH)
print(f" Path: {resolved}")
if not os.path.exists(resolved):
print(f" ✗ File not found: {resolved}")
print(" Make sure oliver_box_config.json is in the project root.")
return False
try:
with open(resolved) as f:
config = json.load(f)
app_settings = config.get('boxAppSettings', {})
client_id = app_settings.get('clientID', '')
enterprise_id = config.get('enterpriseID', '')
key_id = app_settings.get('appAuth', {}).get('publicKeyID', '')
print(f" ✓ File found and valid JSON")
print(f" Client ID: {client_id[:8]}{'*' * (len(client_id) - 8) if len(client_id) > 8 else ''}")
print(f" Enterprise ID: {enterprise_id}")
print(f" Public Key ID: {key_id}")
return True
except json.JSONDecodeError as e:
print(f" ✗ Invalid JSON: {e}")
return False
except Exception as e:
print(f" ✗ Error reading config: {e}")
return False
def check_env_vars():
print_section("2. Checking Environment Variables (.env)")
vars_to_check = {
'BOX_CONFIG_PATH': BOX_CONFIG_PATH,
'BOX_VIDEO_OPTIMIZER_FOLDER_ID': BOX_VIDEO_OPTIMIZER_FOLDER_ID,
'BOX_AS_USER_ID': BOX_AS_USER_ID or '(not set — will use service account)',
'BOX_WEBHOOK_SECRET': os.getenv('BOX_WEBHOOK_SECRET', '(not set)'),
'BOX_PROCESSOR_PORT': os.getenv('BOX_PROCESSOR_PORT', '5001 (default)'),
'BOX_USE_POLLING': os.getenv('BOX_USE_POLLING', 'false (default)'),
'BOX_POLL_INTERVAL_SECONDS': os.getenv('BOX_POLL_INTERVAL_SECONDS', '60 (default)'),
}
all_ok = True
for key, value in vars_to_check.items():
status = '' if value and value != '(not set)' else ''
if key == 'BOX_AS_USER_ID':
status = '' # optional but flagged
print(f" {status} {key}: {value}")
if key == 'BOX_VIDEO_OPTIMIZER_FOLDER_ID' and not value:
all_ok = False
if not BOX_VIDEO_OPTIMIZER_FOLDER_ID:
print("\n ✗ BOX_VIDEO_OPTIMIZER_FOLDER_ID is required — add it to .env")
all_ok = False
return all_ok
def authenticate_and_discover():
print_section("3. Authenticating with Box")
try:
from box_client import BoxClient
except ImportError as e:
print(f" ✗ Could not import BoxClient: {e}")
print(" Make sure you are running from the backend/ directory")
print(" and that boxsdk is installed: pip install boxsdk[jwt]")
return None
client = BoxClient(
config_path=os.path.abspath(BOX_CONFIG_PATH),
as_user_id=BOX_AS_USER_ID
)
if not client.authenticate():
print(" ✗ Authentication failed — check credentials in oliver_box_config.json")
return None
return client
def list_users(box_client):
print_section("4. Enterprise Users (to identify BOX_AS_USER_ID)")
if BOX_AS_USER_ID:
print(f" BOX_AS_USER_ID already set: {BOX_AS_USER_ID}")
print(" Skipping user listing. Remove BOX_AS_USER_ID from .env to list all users.")
return
print(" Listing enterprise users so you can identify the correct BOX_AS_USER_ID:")
print(" (The user who owns/manages the VIDEO_OPTIMIZER Box folder)\n")
try:
users = box_client.list_enterprise_users()
if not users:
print(" ⚠ No users returned. The service account may not have user listing rights.")
print(" Ask DJP for the Box user ID of the folder owner.")
except Exception as e:
print(f" ⚠ Could not list users: {e}")
print(" This is not critical — ask DJP for the folder owner's Box user ID.")
def list_accessible_folders(box_client):
"""List folders the service account can already see (shared with it or owned by it)"""
print_section("4b. Folders Accessible to Service Account")
print(" Listing top-level folders the service account can see.")
print(" If VIDEO_OPTIMIZER is here, copy its ID into BOX_VIDEO_OPTIMIZER_FOLDER_ID in .env\n")
try:
items = box_client.client.folder('0').get_items(limit=100)
found = False
for item in items:
found = True
marker = " ← looks like it!" if 'video' in item.name.lower() or 'optimizer' in item.name.lower() else ""
print(f" [{item.type:6}] ID: {item.id:20} Name: {item.name}{marker}")
if not found:
print(" (no items — the service account hasn't had any folders shared with it yet)")
print()
print(" To fix: In Box, right-click the VIDEO_OPTIMIZER folder → Share → Invite")
print(" Invite this email as Editor:")
print(" AutomationUser_2499781_HJdLm0ZhaO@boxdevedition.com")
except Exception as e:
print(f" ⚠ Could not list accessible folders: {e}")
def discover_folders(box_client):
print_section("5. Discovering Box Folders")
if not BOX_VIDEO_OPTIMIZER_FOLDER_ID:
print(" ✗ BOX_VIDEO_OPTIMIZER_FOLDER_ID not set — cannot discover folders")
print(" Set it in .env then re-run this script.")
return {}
print(f" Looking inside folder ID: {BOX_VIDEO_OPTIMIZER_FOLDER_ID}")
folders = box_client.discover_folders(BOX_VIDEO_OPTIMIZER_FOLDER_ID)
if not folders:
print("\n ✗ No sub-folders found.")
print(" Make sure the following folders exist inside the VIDEO_OPTIMIZER folder:")
print(" IN — drop raw videos here")
print(" OUT_SUCCESS — successfully optimised videos + reports")
print(" OUT_FAILED — error reports for invalid/failed files")
print("\n Also ensure the Box service account has been shared on the VIDEO_OPTIMIZER folder.")
else:
required = ['IN', 'OUT_SUCCESS', 'OUT_FAILED']
missing = [r for r in required if r not in folders]
if missing:
print(f"\n ⚠ Missing folders: {missing}")
print(" Create these folders inside the VIDEO_OPTIMIZER folder in Box.")
return folders
def print_summary(folders):
print_section("6. Summary & Next Steps")
ready = bool(folders and all(f in folders for f in ['IN', 'OUT_SUCCESS', 'OUT_FAILED']))
if ready:
print(" ✓ Box integration is configured and folders are accessible.")
print("\n To start the service:")
print(" # Development (polling mode — no public URL needed):")
print(" BOX_USE_POLLING=true python backend/box_processor.py")
print()
print(" # Development (webhook mode — requires ngrok):")
print(" ngrok http 5001")
print(" # Then configure a Box webhook pointing to: https://<ngrok-url>/webhooks/box")
print(" python backend/box_processor.py")
print()
print(" # Test a specific file from the IN folder:")
print(" python backend/test_box_processor.py")
print()
print(" Folder IDs to note:")
for name, fid in folders.items():
print(f" {name:15}{fid}")
else:
print(" ✗ Setup incomplete — resolve the issues above before starting the service.")
print()
print(" Common fixes:")
print(" 1. Create IN, OUT_SUCCESS, OUT_FAILED folders inside the VIDEO_OPTIMIZER folder")
print(" 2. Share the VIDEO_OPTIMIZER folder with the Box service account")
print(" 3. Set BOX_AS_USER_ID in .env if the folder is owned by a specific user")
print(" 4. Contact DJP for the folder IDs and correct user ID")
if not BOX_AS_USER_ID:
print()
print(" ⚠ BOX_AS_USER_ID is not set.")
print(" If the VIDEO_OPTIMIZER folder is owned by a specific Box user (not the")
print(" service account), set BOX_AS_USER_ID in .env to that user's ID.")
print(" Use the user list printed in step 4 above to identify the correct ID.")
if not os.getenv('BOX_WEBHOOK_SECRET'):
print()
print(" ⚠ BOX_WEBHOOK_SECRET is not set.")
print(" This is only required when using webhook mode.")
print(" Generate one and set it in both .env and the Box webhook configuration.")
print()
def main():
print("\n" + "=" * 70)
print(" BOX.COM SETUP & DIAGNOSTIC")
print("=" * 70)
# Step 1
if not check_config_file():
sys.exit(1)
# Step 2
check_env_vars()
# Step 3
box_client = authenticate_and_discover()
if not box_client:
sys.exit(1)
# Step 4
list_users(box_client)
# Step 4b
list_accessible_folders(box_client)
# Step 5
folders = discover_folders(box_client)
# Step 6
print_summary(folders)
if __name__ == '__main__':
main()

View file

@ -1,204 +0,0 @@
"""
Conversion logging module for tracking video optimization jobs
Logs conversion details to daily JSON files for audit trail and analytics
"""
import json
import os
from datetime import datetime
from typing import Optional
class ConversionLogger:
"""Handles logging of video conversion operations to JSON files"""
def __init__(self, logs_folder: str):
"""
Initialize the conversion logger
Args:
logs_folder: Path to the folder where log files will be stored
"""
self.logs_folder = logs_folder
os.makedirs(logs_folder, exist_ok=True)
def _get_log_file_path(self, date: Optional[str] = None) -> str:
"""
Get the path to the log file for a specific date
Args:
date: Date string in YYYY-MM-DD format. If None, uses today's date
Returns:
Full path to the log file
"""
if date is None:
date = datetime.now().strftime('%Y-%m-%d')
filename = f"{date}_conversions.json"
return os.path.join(self.logs_folder, filename)
def _read_log_file(self, file_path: str) -> list:
"""
Read existing log entries from a file
Args:
file_path: Path to the log file
Returns:
List of log entries, or empty list if file doesn't exist
"""
if not os.path.exists(file_path):
return []
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
# If file is corrupted or empty, return empty list
return []
def _write_log_file(self, file_path: str, entries: list):
"""
Write log entries to a file
Args:
file_path: Path to the log file
entries: List of log entries to write
"""
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(entries, f, indent=2, ensure_ascii=False)
def log_conversion(
self,
user_email: str,
platform: str,
aspect_ratio: str,
input_file_size: int,
output_file_size: int,
conversion_duration: float,
status: str,
file_id: str,
error_message: Optional[str] = None
):
"""
Log a video conversion operation
Args:
user_email: Email of the user who performed the conversion
platform: Target platform (e.g., 'tiktok', 'meta')
aspect_ratio: Video aspect ratio (e.g., '9:16', '16:9')
input_file_size: Size of input file in bytes
output_file_size: Size of output file in bytes
conversion_duration: Time taken for conversion in seconds
status: Status of conversion ('success' or 'failure')
file_id: Unique identifier for the file
error_message: Error message if conversion failed (optional)
"""
# Calculate size reduction percentage
if input_file_size > 0:
size_reduction = ((input_file_size - output_file_size) / input_file_size) * 100
else:
size_reduction = 0.0
# Create log entry
log_entry = {
'timestamp': datetime.now().isoformat(),
'user_email': user_email,
'platform': platform,
'aspect_ratio': aspect_ratio,
'input_file_size': input_file_size,
'output_file_size': output_file_size,
'size_reduction_percent': round(size_reduction, 2),
'conversion_duration_seconds': round(conversion_duration, 2),
'status': status,
'file_id': file_id,
'error_message': error_message
}
# Get log file path for today
log_file_path = self._get_log_file_path()
# Read existing entries
entries = self._read_log_file(log_file_path)
# Append new entry
entries.append(log_entry)
# Write back to file
self._write_log_file(log_file_path, entries)
return log_entry
def get_logs(self, date: Optional[str] = None) -> list:
"""
Retrieve logs for a specific date
Args:
date: Date string in YYYY-MM-DD format. If None, returns today's logs
Returns:
List of log entries for the specified date
"""
log_file_path = self._get_log_file_path(date)
return self._read_log_file(log_file_path)
def get_user_logs(self, user_email: str, date: Optional[str] = None) -> list:
"""
Retrieve logs for a specific user on a specific date
Args:
user_email: Email of the user
date: Date string in YYYY-MM-DD format. If None, uses today's date
Returns:
List of log entries for the specified user and date
"""
all_logs = self.get_logs(date)
return [log for log in all_logs if log.get('user_email') == user_email]
def get_statistics(self, date: Optional[str] = None) -> dict:
"""
Get statistics for conversions on a specific date
Args:
date: Date string in YYYY-MM-DD format. If None, uses today's date
Returns:
Dictionary containing statistics
"""
logs = self.get_logs(date)
if not logs:
return {
'total_conversions': 0,
'successful_conversions': 0,
'failed_conversions': 0,
'total_input_size': 0,
'total_output_size': 0,
'average_size_reduction': 0.0,
'average_duration': 0.0
}
successful = [log for log in logs if log['status'] == 'success']
failed = [log for log in logs if log['status'] == 'failure']
total_input = sum(log['input_file_size'] for log in logs)
total_output = sum(log['output_file_size'] for log in logs)
if successful:
avg_reduction = sum(log['size_reduction_percent'] for log in successful) / len(successful)
avg_duration = sum(log['conversion_duration_seconds'] for log in successful) / len(successful)
else:
avg_reduction = 0.0
avg_duration = 0.0
return {
'total_conversions': len(logs),
'successful_conversions': len(successful),
'failed_conversions': len(failed),
'total_input_size': total_input,
'total_output_size': total_output,
'average_size_reduction': round(avg_reduction, 2),
'average_duration': round(avg_duration, 2)
}

View file

@ -1,181 +0,0 @@
"""
File cleanup script for video optimizer
Automatically deletes old files from uploads and outputs folders
Can be run manually or scheduled via cron job
"""
import os
import time
from datetime import datetime
from dotenv import load_dotenv
class FileCleanup:
"""Handles cleanup of old video files"""
def __init__(self, uploads_folder: str, outputs_folder: str, max_age_hours: int):
"""
Initialize file cleanup
Args:
uploads_folder: Path to uploads folder
outputs_folder: Path to outputs folder
max_age_hours: Maximum age of files in hours before deletion
"""
self.uploads_folder = uploads_folder
self.outputs_folder = outputs_folder
self.max_age_seconds = max_age_hours * 3600
def get_file_age_seconds(self, file_path: str) -> float:
"""
Get age of file in seconds
Args:
file_path: Path to file
Returns:
Age of file in seconds
"""
try:
file_mtime = os.path.getmtime(file_path)
current_time = time.time()
return current_time - file_mtime
except OSError:
return 0
def cleanup_folder(self, folder_path: str) -> dict:
"""
Clean up old files in a folder
Args:
folder_path: Path to folder to clean
Returns:
Dictionary with cleanup statistics
"""
if not os.path.exists(folder_path):
return {
'folder': folder_path,
'files_deleted': 0,
'space_freed_bytes': 0,
'errors': ['Folder does not exist']
}
files_deleted = 0
space_freed = 0
errors = []
try:
for filename in os.listdir(folder_path):
file_path = os.path.join(folder_path, filename)
# Skip if not a file
if not os.path.isfile(file_path):
continue
# Check file age
file_age = self.get_file_age_seconds(file_path)
if file_age > self.max_age_seconds:
try:
# Get file size before deletion
file_size = os.path.getsize(file_path)
# Delete file
os.remove(file_path)
files_deleted += 1
space_freed += file_size
print(f"Deleted: {filename} (age: {file_age / 3600:.1f} hours, size: {file_size / 1024 / 1024:.2f} MB)")
except Exception as e:
error_msg = f"Failed to delete {filename}: {str(e)}"
errors.append(error_msg)
print(f"ERROR: {error_msg}")
except Exception as e:
errors.append(f"Error scanning folder: {str(e)}")
print(f"ERROR: {str(e)}")
return {
'folder': folder_path,
'files_deleted': files_deleted,
'space_freed_bytes': space_freed,
'space_freed_mb': round(space_freed / 1024 / 1024, 2),
'errors': errors
}
def cleanup_all(self) -> dict:
"""
Clean up old files in both uploads and outputs folders
Returns:
Dictionary with cleanup statistics for both folders
"""
print("=" * 80)
print("FILE CLEANUP - STARTING")
print("=" * 80)
print(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Max file age: {self.max_age_seconds / 3600} hours")
print()
# Cleanup uploads folder
print("Cleaning uploads folder...")
uploads_result = self.cleanup_folder(self.uploads_folder)
print(f"Uploads: Deleted {uploads_result['files_deleted']} files, freed {uploads_result['space_freed_mb']} MB")
print()
# Cleanup outputs folder
print("Cleaning outputs folder...")
outputs_result = self.cleanup_folder(self.outputs_folder)
print(f"Outputs: Deleted {outputs_result['files_deleted']} files, freed {outputs_result['space_freed_mb']} MB")
print()
# Calculate totals
total_files = uploads_result['files_deleted'] + outputs_result['files_deleted']
total_space_mb = uploads_result['space_freed_mb'] + outputs_result['space_freed_mb']
all_errors = uploads_result['errors'] + outputs_result['errors']
print("=" * 80)
print(f"TOTAL: Deleted {total_files} files, freed {total_space_mb} MB")
if all_errors:
print(f"ERRORS: {len(all_errors)} errors occurred")
print("=" * 80)
return {
'timestamp': datetime.now().isoformat(),
'max_age_hours': self.max_age_seconds / 3600,
'uploads': uploads_result,
'outputs': outputs_result,
'total_files_deleted': total_files,
'total_space_freed_mb': total_space_mb,
'errors': all_errors
}
def main():
"""Main function for standalone execution"""
# Load environment variables
load_dotenv()
# Get configuration from environment
backend_dir = os.path.dirname(os.path.abspath(__file__))
uploads_folder = os.path.join(backend_dir, 'uploads')
outputs_folder = os.path.join(backend_dir, 'outputs')
max_age_hours = int(os.getenv('FILE_RETENTION_HOURS', '24'))
# Create cleanup instance
cleanup = FileCleanup(uploads_folder, outputs_folder, max_age_hours)
# Run cleanup
result = cleanup.cleanup_all()
# Return exit code based on errors
if result['errors']:
return 1 # Exit with error code if any errors occurred
return 0 # Exit with success code
if __name__ == '__main__':
exit(main())

View file

@ -1,41 +0,0 @@
[
{
"timestamp": "2025-12-15T17:56:11.234820",
"user_email": "manish.tanwar@brandtech.plus",
"platform": "tiktok",
"aspect_ratio": "1:1",
"input_file_size": 1393426,
"output_file_size": 475721,
"size_reduction_percent": 65.86,
"conversion_duration_seconds": 1.97,
"status": "success",
"file_id": "c0475bdb-e0c4-4f8f-82e0-53df3311583e",
"error_message": null
},
{
"timestamp": "2025-12-15T18:07:40.833086",
"user_email": "manish.tanwar@brandtech.plus",
"platform": "snapchat",
"aspect_ratio": "9:16",
"input_file_size": 5039825,
"output_file_size": 1798765,
"size_reduction_percent": 64.31,
"conversion_duration_seconds": 1.8,
"status": "success",
"file_id": "8974dad0-0e18-4095-8c21-919c610c3587",
"error_message": null
},
{
"timestamp": "2025-12-15T18:08:22.925711",
"user_email": "manish.tanwar@brandtech.plus",
"platform": "tiktok",
"aspect_ratio": "1:1",
"input_file_size": 475721,
"output_file_size": 424316,
"size_reduction_percent": 10.81,
"conversion_duration_seconds": 2.08,
"status": "success",
"file_id": "9cffb687-55dd-44a7-bf62-6df1bb1a6b1a",
"error_message": null
}
]

View file

@ -1,15 +0,0 @@
[
{
"timestamp": "2026-02-12T16:33:29.127117",
"user_email": "anonymous@local",
"platform": "meta",
"aspect_ratio": "1:1",
"input_file_size": 3752110,
"output_file_size": 1479103,
"size_reduction_percent": 60.58,
"conversion_duration_seconds": 0.76,
"status": "success",
"file_id": "8e0c2b17-f5f2-4aca-9b9f-a431b66176ca",
"error_message": null
}
]

View file

@ -1,67 +0,0 @@
[
{
"timestamp": "2026-02-24T12:13:42.339758",
"user_email": "manish.tanwar@brandtech.plus",
"platform": "tiktok",
"aspect_ratio": "1:1",
"input_file_size": 3752110,
"output_file_size": 811946,
"size_reduction_percent": 78.36,
"conversion_duration_seconds": 1.16,
"status": "success",
"file_id": "034a6fca-e92c-45c6-be28-8d97ce61c898",
"error_message": null
},
{
"timestamp": "2026-02-24T12:27:51.089875",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 0,
"output_file_size": 0,
"size_reduction_percent": 0.0,
"conversion_duration_seconds": 4.51,
"status": "failure",
"file_id": "2110559542857",
"error_message": "Failed to download file from Box"
},
{
"timestamp": "2026-02-24T12:42:10.779102",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 0,
"output_file_size": 0,
"size_reduction_percent": 0.0,
"conversion_duration_seconds": 4.47,
"status": "failure",
"file_id": "2144852661548",
"error_message": "Failed to download file from Box"
},
{
"timestamp": "2026-02-24T14:27:31.429838",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 0,
"output_file_size": 0,
"size_reduction_percent": 0.0,
"conversion_duration_seconds": 4.35,
"status": "failure",
"file_id": "2110559542857",
"error_message": "Failed to download file from Box"
},
{
"timestamp": "2026-02-24T14:29:12.418754",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 0,
"output_file_size": 0,
"size_reduction_percent": 0.0,
"conversion_duration_seconds": 4.65,
"status": "failure",
"file_id": "2110559542857",
"error_message": "Failed to download file from Box"
}
]

View file

@ -1,15 +0,0 @@
[
{
"timestamp": "2026-02-25T10:09:34.785361",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 0,
"output_file_size": 0,
"size_reduction_percent": 0.0,
"conversion_duration_seconds": 5.32,
"status": "failure",
"file_id": "2110559542857",
"error_message": "Failed to download file from Box"
}
]

View file

@ -1,67 +0,0 @@
[
{
"timestamp": "2026-03-10T15:20:21.849401",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 3017463,
"output_file_size": 404827,
"size_reduction_percent": 86.58,
"conversion_duration_seconds": 8.28,
"status": "success",
"file_id": "2144852661548",
"error_message": null
},
{
"timestamp": "2026-03-10T15:27:36.777419",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 142025887,
"output_file_size": 2752307,
"size_reduction_percent": 98.06,
"conversion_duration_seconds": 58.2,
"status": "success",
"file_id": "2110559542857",
"error_message": null
},
{
"timestamp": "2026-03-10T16:18:08.941089",
"user_email": "manish.tanwar@brandtech.plus",
"platform": "amazon_prime",
"aspect_ratio": "16:9",
"input_file_size": 3627980,
"output_file_size": 4242893,
"size_reduction_percent": -16.95,
"conversion_duration_seconds": 1.62,
"status": "success",
"file_id": "6f02a8e1-1a41-4262-9367-6d0113a5d970",
"error_message": null
},
{
"timestamp": "2026-03-10T16:19:39.675452",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 112004766,
"output_file_size": 2611071,
"size_reduction_percent": 97.67,
"conversion_duration_seconds": 178.87,
"status": "success",
"file_id": "2110554249247",
"error_message": null
},
{
"timestamp": "2026-03-10T16:20:06.536122",
"user_email": "box_automation@system",
"platform": "tiktok",
"aspect_ratio": "9:16",
"input_file_size": 112004766,
"output_file_size": 0,
"size_reduction_percent": 100.0,
"conversion_duration_seconds": 202.92,
"status": "failure",
"file_id": "2110554249247",
"error_message": "Failed to upload optimised video to OUT_SUCCESS"
}
]

0
backend/outputs/.gitkeep Normal file
View file

View file

@ -2,5 +2,3 @@ Flask==3.0.0
Flask-CORS==4.0.0
ffmpeg-python==0.2.0
Werkzeug==3.0.1
python-dotenv==1.2.1
boxsdk[jwt]==3.9.2

View file

@ -1,18 +0,0 @@
#!/usr/bin/env python3
"""
Wrapper script for file cleanup cron job
This script can be scheduled in crontab to run periodically
"""
import sys
import os
# Add backend directory to Python path
backend_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, backend_dir)
# Import and run cleanup
from file_cleanup import main
if __name__ == '__main__':
sys.exit(main())

View file

@ -1,114 +0,0 @@
"""
Test script for Box automation endpoints.
The Box processor is now integrated into app.py on port 5000.
Usage:
# Start the main server first
python app.py
# Then run tests
python test_box_processor.py
"""
import requests
import json
BASE_URL = "http://localhost:5000"
def test_health():
"""Test Box automation health endpoint"""
print("\n" + "=" * 70)
print("TEST: Box Automation Health")
print("=" * 70)
try:
response = requests.get(f"{BASE_URL}/api/box/health")
print(f"Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
if response.status_code == 200:
data = response.json()
if data.get('box_initialised'):
print("✓ Box is initialised and ready")
else:
print("⚠ Box is not initialised — check BOX_VIDEO_OPTIMIZER_FOLDER_ID in .env")
return True
return False
except requests.exceptions.ConnectionError:
print("✗ Connection refused — is app.py running on port 5000?")
print(" Start with: python backend/app.py")
return False
except Exception as e:
print(f"✗ Error: {e}")
return False
def test_manual_trigger(file_id: str, filename: str):
"""Test manual processing trigger with a real Box file ID"""
print("\n" + "=" * 70)
print(f"TEST: Manual Trigger — {filename}")
print("=" * 70)
try:
response = requests.post(
f"{BASE_URL}/api/box/trigger",
json={'file_id': file_id, 'filename': filename},
timeout=300 # 5 min — allow for full download + FFmpeg conversion
)
print(f"Status: {response.status_code}")
print(json.dumps(response.json(), indent=2))
if response.status_code == 200:
result = response.json()
status = result.get('status')
if status == 'success':
print("✓ Processing successful")
return True
elif status == 'skipped':
print(f"⚠ Skipped — invalid filename: {result.get('error')}")
return True
else:
print(f"✗ Failed: {result.get('error')}")
return False
return False
except requests.exceptions.Timeout:
print("✗ Request timed out — processing may still be running on server")
return False
except Exception as e:
print(f"✗ Error: {e}")
return False
def run_tests():
print("\n" + "=" * 70)
print("BOX AUTOMATION — TEST SUITE")
print(f"Target: {BASE_URL}")
print("=" * 70)
if not test_health():
print("\n✗ Health check failed — stopping tests")
return
print("\n📝 To test with a real Box file:")
print(" 1. Upload a video to the Box IN folder")
print(" 2. Get the file ID from the Box URL or Box admin")
print(" 3. Uncomment and update the test below, then re-run")
print()
print(" Example valid filename: my_campaign_tiktok_9x16.mp4")
print(" Example invalid: my_campaign_no_platform.mp4")
# Uncomment to test with a real Box file ID:
# test_manual_trigger("YOUR_BOX_FILE_ID", "my_campaign_tiktok_9x16.mp4")
# test_manual_trigger("YOUR_BOX_FILE_ID", "invalid_no_platform.mp4") # tests error path
print("\n" + "=" * 70)
print("DONE")
print("=" * 70)
if __name__ == '__main__':
run_tests()

0
backend/uploads/.gitkeep Normal file
View file

View file

@ -1,68 +0,0 @@
# ==============================================================================
# VIDEO OPTIMIZER - PRODUCTION ENVIRONMENT CONFIGURATION
# ==============================================================================
# Copy this file to /opt/video-optimizer-back/.env on your production server
# and update the values below
# ==============================================================================
# ------------------------------------------------------------------------------
# ENVIRONMENT SETTING
# ------------------------------------------------------------------------------
FLASK_ENV=production
# ------------------------------------------------------------------------------
# AZURE AD CONFIGURATION (Microsoft SSO)
# ------------------------------------------------------------------------------
# Get these values from Azure Portal > App Registrations
# These should match your Azure AD app registration for production
AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
# Production redirect URI - must be registered in Azure AD
REDIRECT_URI=https://ai-sandbox.oliver.solutions/video-optimizer
# ------------------------------------------------------------------------------
# API CONFIGURATION
# ------------------------------------------------------------------------------
# Backend API port (Flask server) - accessed via Apache reverse proxy
BACKEND_PORT=5013
# API Base URL - Served to frontend via /api/config endpoint
# This tells the frontend where to send API requests
API_BASE_URL=https://ai-sandbox.oliver.solutions/video-optimizer/api
# Frontend URL - Used for CORS configuration
FRONTEND_URL=https://ai-sandbox.oliver.solutions/video-optimizer
# ------------------------------------------------------------------------------
# FILE MANAGEMENT
# ------------------------------------------------------------------------------
# Maximum upload file size in MB
MAX_FILE_SIZE_MB=500
# File retention period in hours (files older than this will be auto-deleted)
FILE_RETENTION_HOURS=24
# ------------------------------------------------------------------------------
# APPLICATION SECURITY
# ------------------------------------------------------------------------------
# IMPORTANT: Generate a random secret key for production!
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=CHANGE-THIS-TO-A-RANDOM-SECRET-KEY
# ------------------------------------------------------------------------------
# LOGGING CONFIGURATION
# ------------------------------------------------------------------------------
# Log level for production (WARNING or ERROR recommended)
LOG_LEVEL=WARNING
# ==============================================================================
# DEPLOYMENT NOTES
# ==============================================================================
# 1. NEVER commit this file to version control (it contains secrets)
# 2. Store this file securely on the server at: /opt/video-optimizer-back/.env
# 3. File permissions should be: chmod 600 (readable only by www-data)
# 4. After updating this file, restart the service:
# sudo systemctl restart video-optimizer-backend
# ==============================================================================

View file

@ -1,114 +0,0 @@
# ==============================================================================
# COMPLETE APACHE VIRTUALHOST CONFIGURATION
# ==============================================================================
# Complete VirtualHost configuration for Video Optimizer on Apache
# Domain: ai-sandbox.oliver.solutions/video-optimizer
#
# Installation:
# 1. Copy this file to /etc/apache2/sites-available/video-optimizer.conf
# 2. Enable required Apache modules:
# sudo a2enmod proxy proxy_http headers rewrite ssl
# 3. Enable the site:
# sudo a2ensite video-optimizer
# 4. Test configuration:
# sudo apache2ctl configtest
# 5. Reload Apache:
# sudo systemctl reload apache2
# ==============================================================================
<VirtualHost *:80>
ServerName ai-sandbox.oliver.solutions
ServerAlias www.ai-sandbox.oliver.solutions
# Redirect all HTTP traffic to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName ai-sandbox.oliver.solutions
ServerAlias www.ai-sandbox.oliver.solutions
# SSL Configuration (adjust certificate paths as needed)
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ai-sandbox.oliver.solutions.crt
SSLCertificateKeyFile /etc/ssl/private/ai-sandbox.oliver.solutions.key
SSLCertificateChainFile /etc/ssl/certs/ca-bundle.crt
# Modern SSL/TLS Configuration
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
# Security Headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# HSTS (HTTP Strict Transport Security) - enable after testing
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Logging
ErrorLog ${APACHE_LOG_DIR}/video-optimizer-error.log
CustomLog ${APACHE_LOG_DIR}/video-optimizer-access.log combined
# ==============================================================================
# VIDEO OPTIMIZER APPLICATION
# ==============================================================================
# Frontend - Serve static files from /var/www/html/video-optimizer/
Alias /video-optimizer /var/www/html/video-optimizer
<Directory /var/www/html/video-optimizer>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
# Cache static assets
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
Header set Cache-Control "public, max-age=31536000"
</FilesMatch>
# Don't cache HTML files
<FilesMatch "\.(html)$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
</IfModule>
</Directory>
# Backend API - Proxy to Flask backend on localhost:5000
<Location /video-optimizer/api>
ProxyPass http://127.0.0.1:5000/api
ProxyPassReverse http://127.0.0.1:5000/api
# Set proxy timeout (increase for large video uploads)
ProxyTimeout 600
# Preserve host header
ProxyPreserveHost On
# Add X-Forwarded headers
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</Location>
# ==============================================================================
# DEFAULT DOCUMENT ROOT (if you have other content on this domain)
# ==============================================================================
DocumentRoot /var/www/html
<Directory /var/www/html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View file

@ -1,49 +0,0 @@
# ==============================================================================
# MINIMAL APACHE CONFIGURATION (ProxyPass only)
# ==============================================================================
# Add these directives to your existing VirtualHost configuration
# Use this if you already have a VirtualHost for ai-sandbox.oliver.solutions
# ==============================================================================
# STEP 1: Enable required Apache modules (run once)
# sudo a2enmod proxy proxy_http headers
# STEP 2: Add these directives inside your existing <VirtualHost *:443> block
# ------------------------------------------------------------------------------
# Frontend - Serve Video Optimizer static files
# ------------------------------------------------------------------------------
Alias /video-optimizer /var/www/html/video-optimizer
<Directory /var/www/html/video-optimizer>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
# Cache static assets
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$">
Header set Cache-Control "public, max-age=31536000"
</FilesMatch>
# Don't cache HTML files
<FilesMatch "\.(html)$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>
</Directory>
# ------------------------------------------------------------------------------
# Backend API - Proxy to Flask backend
# ------------------------------------------------------------------------------
<Location /video-optimizer/api>
ProxyPass http://127.0.0.1:5000/api
ProxyPassReverse http://127.0.0.1:5000/api
ProxyTimeout 600
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
</Location>
# ------------------------------------------------------------------------------
# STEP 3: After adding these directives, test and reload Apache
# ------------------------------------------------------------------------------
# sudo apache2ctl configtest
# sudo systemctl reload apache2

View file

@ -1,40 +0,0 @@
# DEPRECATED — Box automation is now integrated into the main app.py service.
# Use deployment/video-optimizer-backend.service instead.
# This file is kept for reference only.
[Unit]
Description=Box Video Processor Service (DEPRECATED — use video-optimizer-backend.service)
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/video-optimizer-back/backend
# Environment setup
Environment="PATH=/opt/video-optimizer-back/venv/bin:/usr/local/bin:/usr/bin:/bin"
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=/opt/video-optimizer-back/.env
# DEPRECATED: Box automation now runs inside app.py, not box_processor.py standalone
ExecStart=/opt/video-optimizer-back/venv/bin/python box_processor.py
# Restart policy
Restart=always
RestartSec=10
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=box-processor
# Security settings
NoNewPrivileges=true
PrivateTmp=true
# File permissions
UMask=0002
[Install]
WantedBy=multi-user.target

View file

@ -1,289 +0,0 @@
#!/bin/bash
################################################################################
# Video Optimizer - Deployment Script
################################################################################
# This script deploys the backend application to /opt/video-optimizer-back
# Run this script AFTER copying files to /opt/video-optimizer-back
#
# Usage:
# sudo bash deploy.sh [--initial]
#
# Options:
# --initial First-time deployment (creates venv, installs dependencies)
# On subsequent runs without this flag, script will:
# - Update code
# - Restart the systemd service
################################################################################
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
APP_DIR="/opt/video-optimizer-back"
VENV_DIR="$APP_DIR/venv"
BACKEND_DIR="$APP_DIR/backend"
SERVICE_NAME="video-optimizer-backend"
APP_USER="www-data"
APP_GROUP="www-data"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}ERROR: This script must be run as root (use sudo)${NC}"
exit 1
fi
# Check if initial deployment flag is set
INITIAL_DEPLOY=false
if [ "$1" == "--initial" ]; then
INITIAL_DEPLOY=true
fi
################################################################################
# Functions
################################################################################
print_header() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
################################################################################
# Main Deployment
################################################################################
print_header "Video Optimizer - Backend Deployment"
# Check if app directory exists
if [ ! -d "$APP_DIR" ]; then
print_error "Application directory not found: $APP_DIR"
print_info "Please copy application files to $APP_DIR first"
exit 1
fi
print_success "Application directory found: $APP_DIR"
# Change to app directory
cd "$APP_DIR"
################################################################################
# Initial Setup (only on first deployment)
################################################################################
if [ "$INITIAL_DEPLOY" = true ]; then
print_header "Initial Deployment Setup"
# Check if Python 3 is installed
if ! command -v python3 &> /dev/null; then
print_error "Python 3 is not installed"
print_info "Install with: sudo apt-get install python3 python3-venv python3-pip"
exit 1
fi
print_success "Python 3 found: $(python3 --version)"
# Check if FFmpeg is installed
if ! command -v ffmpeg &> /dev/null; then
print_warning "FFmpeg is not installed - video conversion will not work!"
print_info "Install with: sudo apt-get install ffmpeg"
read -p "Continue anyway? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
else
print_success "FFmpeg found: $(ffmpeg -version | head -n1)"
fi
# Create virtual environment
print_info "Creating Python virtual environment..."
if [ -d "$VENV_DIR" ]; then
print_warning "Virtual environment already exists, removing old one..."
rm -rf "$VENV_DIR"
fi
python3 -m venv "$VENV_DIR"
print_success "Virtual environment created"
# Activate virtual environment and install dependencies
print_info "Installing Python dependencies..."
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install -r "$BACKEND_DIR/requirements.txt"
deactivate
print_success "Dependencies installed"
# Check if .env file exists
if [ ! -f "$APP_DIR/.env" ]; then
print_error ".env file not found!"
print_info "Please create $APP_DIR/.env from .env.example"
print_info "Required settings:"
print_info " - FLASK_ENV=production"
print_info " - AZURE_CLIENT_ID=<your-client-id>"
print_info " - AZURE_TENANT_ID=<your-tenant-id>"
print_info " - REDIRECT_URI=https://ai-sandbox.oliver.solutions/video-optimizer"
print_info " - FRONTEND_URL=https://ai-sandbox.oliver.solutions/video-optimizer"
print_info " - SECRET_KEY=<generate-random-key>"
exit 1
fi
print_success ".env file found"
else
print_header "Updating Existing Deployment"
# Check if venv exists
if [ ! -d "$VENV_DIR" ]; then
print_error "Virtual environment not found!"
print_info "Run with --initial flag for first-time setup: sudo bash deploy.sh --initial"
exit 1
fi
print_success "Virtual environment found"
# Update dependencies (in case requirements.txt changed)
print_info "Updating Python dependencies..."
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install -r "$BACKEND_DIR/requirements.txt"
deactivate
print_success "Dependencies updated"
fi
################################################################################
# Create Required Directories
################################################################################
print_info "Creating required directories..."
mkdir -p "$BACKEND_DIR/uploads"
mkdir -p "$BACKEND_DIR/outputs"
mkdir -p "$BACKEND_DIR/logs"
mkdir -p "$BACKEND_DIR/logs/conversions"
print_success "Directories created"
################################################################################
# Set Permissions
################################################################################
print_info "Setting file permissions..."
# Set ownership to www-data for entire app directory
chown -R $APP_USER:$APP_GROUP "$APP_DIR"
# Ensure www-data can write to working directories
chmod -R 755 "$APP_DIR"
chmod -R 775 "$BACKEND_DIR/uploads"
chmod -R 775 "$BACKEND_DIR/outputs"
chmod -R 775 "$BACKEND_DIR/logs"
# Protect .env file (readable only by owner)
if [ -f "$APP_DIR/.env" ]; then
chmod 600 "$APP_DIR/.env"
chown $APP_USER:$APP_GROUP "$APP_DIR/.env"
fi
print_success "Permissions set"
################################################################################
# Service Management
################################################################################
print_header "Service Management"
# Check if service is running
if systemctl is-active --quiet $SERVICE_NAME; then
print_info "Stopping $SERVICE_NAME service..."
systemctl stop $SERVICE_NAME
print_success "Service stopped"
fi
# Reload systemd daemon
print_info "Reloading systemd daemon..."
systemctl daemon-reload
print_success "Systemd daemon reloaded"
# Enable service (if not already enabled)
if ! systemctl is-enabled --quiet $SERVICE_NAME; then
print_info "Enabling $SERVICE_NAME service..."
systemctl enable $SERVICE_NAME
print_success "Service enabled"
else
print_success "Service already enabled"
fi
# Start service
print_info "Starting $SERVICE_NAME service..."
systemctl start $SERVICE_NAME
sleep 2
# Check if service started successfully
if systemctl is-active --quiet $SERVICE_NAME; then
print_success "Service started successfully"
else
print_error "Service failed to start!"
print_info "Check logs with: sudo journalctl -u $SERVICE_NAME -n 50 --no-pager"
exit 1
fi
################################################################################
# Verification
################################################################################
print_header "Deployment Verification"
# Check service status
print_info "Service status:"
systemctl status $SERVICE_NAME --no-pager -l | head -n 10
# Test health endpoint
print_info "Testing health endpoint..."
sleep 3
if curl -f http://localhost:5000/api/health > /dev/null 2>&1; then
print_success "Backend API is responding"
else
print_warning "Backend API is not responding (may still be starting up)"
print_info "Check logs with: sudo journalctl -u $SERVICE_NAME -f"
fi
################################################################################
# Summary
################################################################################
print_header "Deployment Complete!"
echo -e "Backend Service: ${GREEN}Running${NC}"
echo -e "Service Name: $SERVICE_NAME"
echo -e "Application Dir: $APP_DIR"
echo ""
echo "Useful Commands:"
echo " View logs: sudo journalctl -u $SERVICE_NAME -f"
echo " Restart service: sudo systemctl restart $SERVICE_NAME"
echo " Stop service: sudo systemctl stop $SERVICE_NAME"
echo " Service status: sudo systemctl status $SERVICE_NAME"
echo ""
echo "Next Steps:"
echo " 1. Copy frontend files to /var/www/html/video-optimizer"
echo " 2. Configure Apache (see DEPLOYMENT.md)"
echo " 3. Test application: https://ai-sandbox.oliver.solutions/video-optimizer"
echo ""

View file

@ -1,36 +0,0 @@
[Unit]
Description=Video Optimizer Backend API
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/video-optimizer-back/backend
# Environment setup
Environment="PATH=/opt/video-optimizer-back/venv/bin:/usr/local/bin:/usr/bin:/bin"
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=/opt/video-optimizer-back/.env
# Start Flask backend
ExecStart=/opt/video-optimizer-back/venv/bin/python app.py
# Restart policy
Restart=always
RestartSec=10
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=video-optimizer-backend
# Security settings
NoNewPrivileges=true
PrivateTmp=true
# File permissions - ensures www-data can write to uploads/outputs/logs
UMask=0002
[Install]
WantedBy=multi-user.target

View file

@ -1,120 +0,0 @@
# ==============================================================================
# VIDEO OPTIMIZER - FRONTEND SECURITY CONFIGURATION
# ==============================================================================
# Location: /var/www/html/video-optimizer/.htaccess
# Purpose: Security hardening for frontend static files
# ==============================================================================
# ------------------------------------------------------------------------------
# DIRECTORY PROTECTION
# ------------------------------------------------------------------------------
# Disable directory browsing
Options -Indexes
# Follow symbolic links (required for some servers)
Options +FollowSymLinks
# Disable server signature
ServerSignature Off
# ------------------------------------------------------------------------------
# FILE ACCESS CONTROL
# ------------------------------------------------------------------------------
# Default: Allow access to all files (will be restricted below)
<FilesMatch ".*">
Require all granted
</FilesMatch>
# Deny access to sensitive files and patterns
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Deny access to backup and temporary files
<FilesMatch "\.(bak|backup|old|tmp|temp|swp|save|orig|dist|log|sql|sqlite|db)$">
Require all denied
</FilesMatch>
# Deny access to version control files
<FilesMatch "(^\.git|^\.svn|^\.hg|^\.bzr)">
Require all denied
</FilesMatch>
# Deny access to environment and configuration files
<FilesMatch "^(\.env|\.env\.|config\.json|package\.json|package-lock\.json|composer\.json|composer\.lock)">
Require all denied
</FilesMatch>
# Deny access to PHP files (if any exist - security measure)
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
# Deny access to Python files (should not be in frontend)
<FilesMatch "\.py$">
Require all denied
</FilesMatch>
# Deny access to README and documentation that shouldn't be public
<FilesMatch "^(README|INSTALL|CHANGELOG|LICENSE|CONTRIBUTING)">
Require all denied
</FilesMatch>
# ------------------------------------------------------------------------------
# ALLOWED FILE TYPES (Explicitly allow necessary files)
# ------------------------------------------------------------------------------
# Allow HTML files (main application pages)
<FilesMatch "\.(html|htm)$">
Require all granted
</FilesMatch>
# Allow JavaScript files
<FilesMatch "\.(js|mjs)$">
Require all granted
</FilesMatch>
# Allow CSS files
<FilesMatch "\.css$">
Require all granted
</FilesMatch>
# Allow images
<FilesMatch "\.(jpg|jpeg|png|gif|ico|svg|webp)$">
Require all granted
</FilesMatch>
# Allow fonts
<FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
Require all granted
</FilesMatch>
# Allow JSON files (only if needed for app functionality)
<FilesMatch "\.json$">
Require all denied
</FilesMatch>
# ------------------------------------------------------------------------------
# ERROR DOCUMENTS
# ------------------------------------------------------------------------------
# Custom error pages (optional - create these files if needed)
# ErrorDocument 403 /video-optimizer/error/403.html
# ErrorDocument 404 /video-optimizer/error/404.html
# ErrorDocument 500 /video-optimizer/error/500.html
# ------------------------------------------------------------------------------
# ADDITIONAL SECURITY
# ------------------------------------------------------------------------------
# Prevent access to .htaccess itself
<Files ".htaccess">
Require all denied
</Files>
# ==============================================================================
# END OF CONFIGURATION
# ==============================================================================

View file

@ -1,309 +0,0 @@
/**
* Admin Panel Quality of Life Enhancements
* Adds search, filter, and improved UX to the admin panel
*/
// ==============================================================================
// INITIALIZE ADMIN ENHANCEMENTS
// ==============================================================================
document.addEventListener('DOMContentLoaded', () => {
// Add search bar to platforms section
addPlatformSearch();
// Add quick actions
addQuickActions();
// Add keyboard shortcuts
initAdminKeyboardShortcuts();
// Initialize toast and utils
if (typeof KeyboardShortcuts !== 'undefined') {
KeyboardShortcuts.init();
}
console.log('✨ Admin panel enhancements loaded!');
});
// ==============================================================================
// PLATFORM SEARCH & FILTER
// ==============================================================================
function addPlatformSearch() {
const platformsSection = document.querySelector('.platforms-list');
if (!platformsSection) return;
// Create search container
const searchContainer = document.createElement('div');
searchContainer.className = 'search-container';
searchContainer.style.cssText = `
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
`;
// Create search input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'platformSearch';
searchInput.placeholder = '🔍 Search platforms...';
searchInput.className = 'text-input';
searchInput.style.cssText = `
flex: 1;
padding: 12px 15px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
`;
// Create filter select
const filterSelect = document.createElement('select');
filterSelect.id = 'platformFilter';
filterSelect.className = 'select-input';
filterSelect.style.cssText = `
padding: 12px 15px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
`;
filterSelect.innerHTML = `
<option value="all">All Codecs</option>
<option value="libx264">H.264</option>
<option value="libx265">H.265</option>
<option value="libvpx-vp9">VP9</option>
`;
// Create clear button
const clearBtn = document.createElement('button');
clearBtn.innerHTML = '✕ Clear';
clearBtn.className = 'btn-secondary';
clearBtn.style.cssText = `
padding: 12px 20px;
background: #333;
border: 1px solid #555;
border-radius: 6px;
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
`;
clearBtn.onmouseover = function() { this.style.background = '#444'; };
clearBtn.onmouseout = function() { this.style.background = '#333'; };
clearBtn.addEventListener('click', () => {
searchInput.value = '';
filterSelect.value = 'all';
filterPlatforms();
if (typeof toast !== 'undefined') {
toast.info('Filters cleared');
}
});
// Add event listeners
searchInput.addEventListener('input', debounce(filterPlatforms, 300));
filterSelect.addEventListener('change', filterPlatforms);
// Assemble search container
searchContainer.appendChild(searchInput);
searchContainer.appendChild(filterSelect);
searchContainer.appendChild(clearBtn);
// Insert before platforms list
platformsSection.parentElement.insertBefore(searchContainer, platformsSection);
// Add result count
const resultCount = document.createElement('div');
resultCount.id = 'platformResultCount';
resultCount.style.cssText = `
margin-bottom: 15px;
color: #999;
font-size: 14px;
`;
platformsSection.parentElement.insertBefore(resultCount, platformsSection);
// Initial filter
filterPlatforms();
}
function filterPlatforms() {
const searchInput = document.getElementById('platformSearch');
const filterSelect = document.getElementById('platformFilter');
const resultCount = document.getElementById('platformResultCount');
const platformCards = document.querySelectorAll('.platform-card');
if (!searchInput || !filterSelect || !platformCards.length) return;
const searchTerm = searchInput.value.toLowerCase();
const codecFilter = filterSelect.value;
let visibleCount = 0;
platformCards.forEach(card => {
const platformName = card.querySelector('h3')?.textContent.toLowerCase() || '';
const platformCodec = card.querySelector('.platform-meta')?.textContent.toLowerCase() || '';
const matchesSearch = platformName.includes(searchTerm);
const matchesCodec = codecFilter === 'all' || platformCodec.includes(codecFilter);
if (matchesSearch && matchesCodec) {
card.style.display = 'block';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Update result count
if (resultCount) {
const total = platformCards.length;
resultCount.textContent = `Showing ${visibleCount} of ${total} platforms`;
if (visibleCount === 0) {
resultCount.innerHTML = `<span style="color: #FFC407;">No platforms match your search</span>`;
}
}
}
// ==============================================================================
// QUICK ACTIONS
// ==============================================================================
function addQuickActions() {
const adminHeader = document.querySelector('.header');
if (!adminHeader) return;
const quickActions = document.createElement('div');
quickActions.className = 'quick-actions';
quickActions.style.cssText = `
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
`;
// Add platform count
const platformCount = document.querySelectorAll('.platform-card').length;
const countBadge = document.createElement('div');
countBadge.style.cssText = `
padding: 10px 20px;
background: rgba(255, 196, 7, 0.1);
border: 1px solid #FFC407;
border-radius: 6px;
color: #FFC407;
font-size: 14px;
font-weight: 600;
`;
countBadge.textContent = `${platformCount} Platforms Configured`;
quickActions.appendChild(countBadge);
adminHeader.appendChild(quickActions);
}
// ==============================================================================
// ADMIN KEYBOARD SHORTCUTS
// ==============================================================================
function initAdminKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + F - Focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
const searchInput = document.getElementById('platformSearch');
if (searchInput) {
searchInput.focus();
searchInput.select();
if (typeof toast !== 'undefined') {
toast.info('Search platforms (Ctrl+F)');
}
}
}
// Ctrl/Cmd + N - Add new platform
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
const addBtn = document.getElementById('addPlatformBtn');
if (addBtn) {
addBtn.click();
if (typeof toast !== 'undefined') {
toast.info('Add new platform (Ctrl+N)');
}
}
}
// Ctrl/Cmd + E - Export
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
e.preventDefault();
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.click();
if (typeof toast !== 'undefined') {
toast.info('Exporting specifications (Ctrl+E)');
}
}
}
});
}
// ==============================================================================
// UTILITY FUNCTIONS
// ==============================================================================
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// ==============================================================================
// ENHANCE ADMIN OPERATIONS
// ==============================================================================
// Override default alerts with toast notifications
if (typeof toast !== 'undefined') {
window.alert = (message) => {
if (message.toLowerCase().includes('error')) {
toast.error(message);
} else if (message.toLowerCase().includes('warning')) {
toast.warning(message);
} else if (message.toLowerCase().includes('success')) {
toast.success(message);
} else {
toast.info(message);
}
};
}
// Add keyboard shortcut hints
setTimeout(() => {
const footer = document.querySelector('footer');
if (footer) {
const hintsDiv = document.createElement('div');
hintsDiv.style.cssText = 'margin-top: 20px; padding: 15px; background: rgba(255,196,7,0.1); border-radius: 6px; font-size: 12px; color: #999;';
hintsDiv.innerHTML = `
<strong style="color: #FFC407;">Keyboard Shortcuts:</strong>
<span style="margin-left: 15px;">Ctrl+F</span> - Search
<span style="margin-left: 15px;">Ctrl+N</span> - New Platform
<span style="margin-left: 15px;">Ctrl+E</span> - Export
<span style="margin-left: 15px;">Ctrl+K</span> - Help
<span style="margin-left: 15px;">ESC</span> - Close dialogs
`;
footer.insertBefore(hintsDiv, footer.firstChild);
}
}, 1000);
console.log('✨ Admin enhancements initialized successfully!');

View file

@ -1,199 +1,5 @@
/* Admin Panel Additional Styles */
/* ============================================================
BOX AUTOMATION SECTION
============================================================ */
.box-section {
background: #111;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
border: 1px solid #222;
}
.box-section h2 {
color: #fff;
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 0.4rem;
}
.box-section .section-description {
color: #888;
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
/* Status grid */
.box-status-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 1rem;
margin-bottom: 1.8rem;
}
@media (max-width: 900px) {
.box-status-grid { grid-template-columns: 1fr 1fr; }
}
.box-status-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1rem 1.2rem;
display: flex;
align-items: center;
gap: 0.8rem;
}
.box-status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #444;
flex-shrink: 0;
}
.box-status-indicator.online { background: #22c55e; box-shadow: 0 0 6px #22c55e88; }
.box-status-indicator.offline { background: #ef4444; }
.box-status-indicator.warning { background: #f59e0b; }
.box-status-title {
color: #fff;
font-size: 0.9rem;
font-weight: 600;
}
.box-status-sub {
color: #888;
font-size: 0.75rem;
margin-top: 0.1rem;
}
.box-stat-label {
color: #888;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.box-stat-value {
color: #FFC407;
font-size: 0.8rem;
font-weight: 600;
margin-top: 0.2rem;
word-break: break-all;
}
/* History controls */
.box-history-controls h3 {
color: #fff;
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.8rem;
}
.box-filter-row {
display: flex;
align-items: center;
gap: 0.8rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.box-filter-row label {
color: #888;
font-size: 0.85rem;
}
.box-date-input {
background: #1a1a1a;
color: #fff;
border: 1px solid #333;
border-radius: 6px;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
font-family: inherit;
}
.box-date-input:focus { outline: none; border-color: #FFC407; }
/* Summary row */
.box-summary-row {
color: #888;
font-size: 0.82rem;
margin-bottom: 0.8rem;
padding: 0.5rem 0.8rem;
background: #1a1a1a;
border-radius: 6px;
border: 1px solid #2a2a2a;
}
/* History table */
.box-history-table-wrap {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #2a2a2a;
}
.box-history-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.box-history-table thead {
background: #1a1a1a;
}
.box-history-table th {
color: #888;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.7rem 1rem;
text-align: left;
border-bottom: 1px solid #2a2a2a;
}
.box-history-table td {
color: #ccc;
padding: 0.7rem 1rem;
border-bottom: 1px solid #1a1a1a;
vertical-align: middle;
}
.box-history-table tbody tr:hover td {
background: #161616;
}
.box-history-table tbody tr:last-child td {
border-bottom: none;
}
.box-empty {
text-align: center;
color: #555;
padding: 2rem !important;
}
.box-badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.box-badge.success { background: #14532d; color: #22c55e; }
.box-badge.failure { background: #450a0a; color: #ef4444; }
.box-badge.skipped { background: #2a2000; color: #f59e0b; }
.header-actions {
margin-top: 1rem;
}

View file

@ -9,39 +9,9 @@
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="admin.css">
<!-- MSAL Browser Library for Microsoft SSO -->
<script src="https://cdn.jsdelivr.net/npm/@azure/msal-browser@3.5.0/lib/msal-browser.min.js"></script>
</head>
<body>
<!-- Login Page (shown when not authenticated) -->
<div id="loginPage" class="login-page" style="display: none;">
<div class="login-container">
<div class="login-card">
<h1>Admin Panel</h1>
<p class="subtitle">Platform Specifications Management</p>
<p class="login-message">Please sign in with your Microsoft account to continue</p>
<button class="btn-login" id="loginBtn">
<span>Sign in with Microsoft</span>
</button>
<div id="loginError" class="error-message" style="display: none;"></div>
</div>
</div>
</div>
<!-- Main Application (shown when authenticated) -->
<div class="container" id="mainApp" style="display: none;">
<!-- Auth Header -->
<div class="auth-header">
<div class="auth-user-info">
<span class="user-email" id="userEmail"></span>
</div>
<div class="auth-actions">
<a href="help.html" class="btn-help" title="Help & Documentation">Help</a>
<a href="index.html" class="btn-home" title="Main Application">Home</a>
<button class="btn-logout" id="logoutBtn">Logout</button>
</div>
</div>
<div class="container">
<!-- Header -->
<header class="header">
<h1>Admin Panel</h1>
@ -125,72 +95,6 @@
</div>
</section>
<!-- Box Automation Section -->
<section class="box-section" id="boxSection">
<h2>Box Automation</h2>
<p class="section-description">Status and processing history for the Box.com automated pipeline.</p>
<!-- Connection Status -->
<div class="box-status-grid" id="boxStatusGrid">
<div class="box-status-card" id="boxStatusCard">
<div class="box-status-indicator" id="boxStatusIndicator"></div>
<div class="box-status-details">
<div class="box-status-title" id="boxStatusTitle">Checking...</div>
<div class="box-status-sub" id="boxStatusSub"></div>
</div>
</div>
<div class="box-status-card">
<div class="box-stat-label">IN Folder</div>
<div class="box-stat-value" id="boxFolderIn"></div>
</div>
<div class="box-status-card">
<div class="box-stat-label">OUT_SUCCESS</div>
<div class="box-stat-value" id="boxFolderSuccess"></div>
</div>
<div class="box-status-card">
<div class="box-stat-label">OUT_FAILED</div>
<div class="box-stat-value" id="boxFolderFailed"></div>
</div>
</div>
<!-- History Controls -->
<div class="box-history-controls">
<h3>Processing History</h3>
<div class="box-filter-row">
<label for="boxHistoryDate">Date:</label>
<input type="date" id="boxHistoryDate" class="text-input box-date-input">
<button class="btn-secondary btn-sm" id="boxRefreshBtn">Refresh</button>
<button class="btn-secondary btn-sm" id="boxLast7Btn">Last 7 days</button>
</div>
</div>
<!-- Summary stats -->
<div class="box-summary-row" id="boxSummaryRow" style="display:none;">
<span id="boxSummaryText"></span>
</div>
<!-- History Table -->
<div class="box-history-table-wrap">
<table class="box-history-table" id="boxHistoryTable">
<thead>
<tr>
<th>Time</th>
<th>Platform</th>
<th>Ratio</th>
<th>Original</th>
<th>Optimised</th>
<th>Reduction</th>
<th>Duration</th>
<th>Status</th>
</tr>
</thead>
<tbody id="boxHistoryBody">
<tr><td colspan="8" class="box-empty">Loading...</td></tr>
</tbody>
</table>
</div>
</section>
<!-- Add/Edit Platform Modal -->
<div class="modal" id="platformModal" style="display: none;">
<div class="modal-content">
@ -254,74 +158,6 @@
</div>
<script src="config.js"></script>
<script src="auth.js"></script>
<script src="toast.js"></script>
<script src="utils.js"></script>
<script src="admin.js"></script>
<script src="admin-enhancements.js"></script>
<script>
// Initialize authentication and check login status
async function initializeApp() {
try {
// Fetch Azure AD configuration from backend
const response = await fetch(`${CONFIG.API_BASE}/config`);
const config = await response.json();
// Initialize MSAL
await window.initAuth(config);
// Check authentication status
if (window.isAuthenticated()) {
// User is authenticated - show main app
const userEmail = window.getUserEmail();
document.getElementById('userEmail').textContent = userEmail || 'User';
document.getElementById('mainApp').style.display = 'block';
document.getElementById('loginPage').style.display = 'none';
} else {
// User not authenticated - show login page
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
}
} catch (error) {
console.error('Initialization error:', error);
// Show error on login page
const loginError = document.getElementById('loginError');
if (loginError) {
loginError.textContent = error.message || 'Failed to initialize authentication. Please try again.';
loginError.style.display = 'block';
}
// Show login page even on error
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
}
}
// Login button handler
document.getElementById('loginBtn').addEventListener('click', async () => {
try {
document.getElementById('loginError').style.display = 'none';
await window.login();
} catch (error) {
console.error('Login error:', error);
const loginError = document.getElementById('loginError');
loginError.textContent = error.message || 'Login failed. Please try again.';
loginError.style.display = 'block';
}
});
// Logout button handler
document.getElementById('logoutBtn').addEventListener('click', async () => {
try {
await window.logout();
} catch (error) {
console.error('Logout error:', error);
}
});
// Initialize app on page load
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>

View file

@ -1,5 +1,6 @@
// Admin Panel JavaScript
// API Configuration is loaded from config.js (API_BASE is available globally)
// API Configuration
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
// State
let platforms = [];
@ -13,8 +14,6 @@ document.addEventListener('DOMContentLoaded', () => {
loadPlatforms();
loadNamingConventions();
setupEventListeners();
loadBoxStatus();
loadBoxHistory();
});
// Event Listeners
@ -35,16 +34,6 @@ function setupEventListeners() {
document.getElementById('addAspectRatioPatternBtn').addEventListener('click', addAspectRatioPattern);
document.getElementById('saveNamingBtn').addEventListener('click', saveNamingConventions);
document.getElementById('testNamingBtn').addEventListener('click', testNaming);
// Box automation history
const today = new Date().toISOString().slice(0, 10);
document.getElementById('boxHistoryDate').value = today;
document.getElementById('boxRefreshBtn').addEventListener('click', () => {
loadBoxStatus();
loadBoxHistory();
});
document.getElementById('boxLast7Btn').addEventListener('click', () => loadBoxHistory(7));
document.getElementById('boxHistoryDate').addEventListener('change', () => loadBoxHistory());
}
// Load Platforms
@ -667,117 +656,3 @@ function testNaming() {
alert(message);
}
// ============================================================
// BOX AUTOMATION HISTORY
// ============================================================
async function loadBoxStatus() {
const indicator = document.getElementById('boxStatusIndicator');
const title = document.getElementById('boxStatusTitle');
const sub = document.getElementById('boxStatusSub');
const inEl = document.getElementById('boxFolderIn');
const successEl = document.getElementById('boxFolderSuccess');
const failedEl = document.getElementById('boxFolderFailed');
try {
const res = await fetch(`${API_BASE}/box/health`);
const data = await res.json();
if (!data.box_available) {
indicator.className = 'box-status-indicator offline';
title.textContent = 'Box SDK not installed';
sub.textContent = 'Run: pip install boxsdk[jwt]';
return;
}
if (!data.box_initialised) {
indicator.className = 'box-status-indicator warning';
title.textContent = 'Box not initialised';
sub.textContent = 'Check BOX_VIDEO_OPTIMIZER_FOLDER_ID in .env';
return;
}
indicator.className = 'box-status-indicator online';
const mode = data.polling_enabled
? `Polling every ${data.polling_interval_seconds}s`
: 'Webhook mode';
title.textContent = `Connected — ${mode}`;
sub.textContent = `Folders configured: ${data.folders_configured ? '✓' : '✗'}`;
const folders = data.folders || {};
inEl.textContent = folders['IN'] ? `ID: ${folders['IN']}` : '—';
successEl.textContent = folders['OUT_SUCCESS'] ? `ID: ${folders['OUT_SUCCESS']}` : '—';
failedEl.textContent = folders['OUT_FAILED'] ? `ID: ${folders['OUT_FAILED']}` : '—';
} catch (err) {
indicator.className = 'box-status-indicator offline';
title.textContent = 'Cannot reach backend';
sub.textContent = err.message;
}
}
async function loadBoxHistory(days = null) {
const tbody = document.getElementById('boxHistoryBody');
const summaryRow = document.getElementById('boxSummaryRow');
const summaryText = document.getElementById('boxSummaryText');
tbody.innerHTML = '<tr><td colspan="8" class="box-empty">Loading...</td></tr>';
summaryRow.style.display = 'none';
try {
let url;
if (days) {
url = `${API_BASE}/admin/box-history?days=${days}`;
} else {
const date = document.getElementById('boxHistoryDate').value;
url = `${API_BASE}/admin/box-history?date=${date}`;
}
const res = await fetch(url);
const data = await res.json();
const jobs = data.jobs || [];
if (jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="box-empty">No Box jobs found for this period.</td></tr>';
return;
}
// Summary
const successful = jobs.filter(j => j.status === 'success').length;
const failed = jobs.filter(j => j.status !== 'success').length;
const label = days ? `Last ${data.days} days` : data.date;
summaryText.textContent = `${label}${jobs.length} job${jobs.length !== 1 ? 's' : ''}: ${successful} succeeded, ${failed} failed`;
summaryRow.style.display = 'block';
// Rows
tbody.innerHTML = jobs.map(job => {
const time = new Date(job.timestamp).toLocaleString('en-GB', {
day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'
});
const inputMb = job.input_file_size ? (job.input_file_size / 1048576).toFixed(1) + ' MB' : '—';
const outputMb = job.output_file_size ? (job.output_file_size / 1048576).toFixed(1) + ' MB' : '—';
const reduction = job.size_reduction_percent != null ? job.size_reduction_percent.toFixed(1) + '%' : '—';
const duration = job.conversion_duration_seconds != null ? job.conversion_duration_seconds.toFixed(0) + 's' : '—';
const platform = (job.platform || '—').toUpperCase();
const ratio = job.aspect_ratio || '—';
const badgeClass = job.status === 'success' ? 'success'
: job.status === 'skipped' ? 'skipped'
: 'failure';
return `<tr>
<td>${time}</td>
<td>${platform}</td>
<td>${ratio}</td>
<td>${inputMb}</td>
<td>${outputMb}</td>
<td>${reduction}</td>
<td>${duration}</td>
<td><span class="box-badge ${badgeClass}">${job.status}</span></td>
</tr>`;
}).join('');
} catch (err) {
tbody.innerHTML = `<tr><td colspan="8" class="box-empty">Error loading history: ${err.message}</td></tr>`;
}
}

View file

@ -1,352 +0,0 @@
/**
* Quality of Life Enhancements for app.js
* This file adds additional functionality to the main app
*/
// ==============================================================================
// INITIALIZE ENHANCEMENTS
// ==============================================================================
document.addEventListener('DOMContentLoaded', () => {
// Initialize keyboard shortcuts
KeyboardShortcuts.init();
// Initialize drag & drop enhancements
const dropZone = document.getElementById('dropZone');
if (dropZone) {
DragDropEnhancer.init(dropZone);
}
// Load last used settings
const lastSettings = Storage.getLastSettings();
if (lastSettings.platform || lastSettings.aspectRatio) {
toast.info(`Remembered your last settings: ${lastSettings.platform || 'No platform'} / ${lastSettings.aspectRatio || 'No aspect ratio'}`);
}
// Add copy buttons to video info section
addCopyButtons();
// Add download both button
addDownloadBothButton();
// Override default alert with toast
window.alert = (message) => {
if (message.toLowerCase().includes('error')) {
toast.error(message);
} else if (message.toLowerCase().includes('warning')) {
toast.warning(message);
} else if (message.toLowerCase().includes('success')) {
toast.success(message);
} else {
toast.info(message);
}
};
console.log('✨ Quality of Life enhancements loaded!');
});
// ==============================================================================
// ENHANCE FILE UPLOAD
// ==============================================================================
// Add event listeners to enhance file upload with toasts and loading states
// These run in addition to the original handlers in app.js
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
if (fileInput) {
// Add our enhancements to file input changes
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
toast.success(`File selected: ${file.name}`);
// Try to restore last settings after upload completes
setTimeout(() => {
restoreLastSettings();
}, 1000);
}
});
}
if (dropZone) {
// Add toast notification for drop events
dropZone.addEventListener('drop', function(e) {
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
toast.success(`File dropped: ${files[0].name}`);
}
});
}
});
// ==============================================================================
// ENHANCE CONVERSION
// ==============================================================================
// Add event listener to enhance conversion with time estimates and settings saving
document.addEventListener('DOMContentLoaded', () => {
const convertBtn = document.getElementById('convertBtn');
if (convertBtn) {
convertBtn.addEventListener('click', function() {
const platformSelect = document.getElementById('platformSelect');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
// Save current settings
if (platformSelect && aspectRatioSelect) {
Storage.saveLastSettings(platformSelect.value, aspectRatioSelect.value);
}
// Show time estimate
if (window.currentVideoInfo && window.currentVideoInfo.size) {
const estimate = estimateConversionTime(window.currentVideoInfo.size, platformSelect?.value);
toast.info(`Estimated time: ${estimate}`);
}
});
}
});
// ==============================================================================
// RESTORE LAST SETTINGS
// ==============================================================================
function restoreLastSettings() {
const lastSettings = Storage.getLastSettings();
const platformSelect = document.getElementById('platformSelect');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
if (lastSettings.platform && platformSelect) {
// Check if platform still exists
const option = platformSelect.querySelector(`option[value="${lastSettings.platform}"]`);
if (option) {
platformSelect.value = lastSettings.platform;
// Trigger change event to load aspect ratios
platformSelect.dispatchEvent(new Event('change'));
// Wait for aspect ratios to load, then restore
setTimeout(() => {
if (lastSettings.aspectRatio && aspectRatioSelect) {
const aspectOption = aspectRatioSelect.querySelector(`option[value="${lastSettings.aspectRatio}"]`);
if (aspectOption) {
aspectRatioSelect.value = lastSettings.aspectRatio;
aspectRatioSelect.dispatchEvent(new Event('change'));
}
}
}, 200);
}
}
}
// ==============================================================================
// ADD COPY BUTTONS
// ==============================================================================
function addCopyButtons() {
// Add copy button to video info section
const videoInfoDiv = document.getElementById('videoInfo');
if (videoInfoDiv) {
// Create copy button
const copyBtn = document.createElement('button');
copyBtn.id = 'copyVideoSpecsBtn';
copyBtn.className = 'btn-secondary';
copyBtn.innerHTML = '📋 Copy Video Specs';
copyBtn.style.marginTop = '15px';
copyBtn.style.display = 'none';
copyBtn.addEventListener('click', () => {
const platformSelect = document.getElementById('platformSelect');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
if (window.currentVideoInfo) {
Clipboard.copyVideoSpecs(
window.currentVideoInfo,
platformSelect?.value,
aspectRatioSelect?.value
);
}
});
videoInfoDiv.appendChild(copyBtn);
}
// Add copy button to comparison section
const comparisonSection = document.getElementById('comparisonSection');
if (comparisonSection) {
const statsDiv = comparisonSection.querySelector('.comparison-stats');
if (statsDiv) {
const copyResultsBtn = document.createElement('button');
copyResultsBtn.id = 'copyResultsBtn';
copyResultsBtn.className = 'btn-secondary';
copyResultsBtn.innerHTML = '📋 Copy Results';
copyResultsBtn.style.marginTop = '15px';
copyResultsBtn.addEventListener('click', () => {
if (window.lastConversionResults) {
Clipboard.copyConversionResults(window.lastConversionResults);
}
});
statsDiv.appendChild(copyResultsBtn);
}
}
}
// ==============================================================================
// ADD DOWNLOAD BOTH BUTTON
// ==============================================================================
function addDownloadBothButton() {
const comparisonSection = document.getElementById('comparisonSection');
if (comparisonSection) {
const buttonsDiv = comparisonSection.querySelector('.comparison-buttons');
if (buttonsDiv) {
const downloadBothBtn = document.createElement('button');
downloadBothBtn.id = 'downloadBothBtn';
downloadBothBtn.className = 'btn-primary';
downloadBothBtn.innerHTML = '📦 Download Both Files';
downloadBothBtn.style.marginLeft = '10px';
downloadBothBtn.addEventListener('click', async () => {
if (!window.currentFileId) {
toast.error('No files available for download');
return;
}
toast.info('Preparing downloads...');
try {
// Download original
const originalUrl = `${API_BASE}/download/original/${window.currentFileId}`;
const originalLink = document.createElement('a');
originalLink.href = originalUrl;
originalLink.download = '';
document.body.appendChild(originalLink);
originalLink.click();
document.body.removeChild(originalLink);
// Wait a bit, then download optimized
setTimeout(() => {
const optimizedUrl = `${API_BASE}/download/optimized/${window.currentFileId}`;
const optimizedLink = document.createElement('a');
optimizedLink.href = optimizedUrl;
optimizedLink.download = '';
document.body.appendChild(optimizedLink);
optimizedLink.click();
document.body.removeChild(optimizedLink);
toast.success('Both files are downloading!');
}, 500);
} catch (error) {
ErrorHandler.show(error);
}
});
buttonsDiv.appendChild(downloadBothBtn);
}
}
}
// ==============================================================================
// MONITOR FOR CONVERSION COMPLETION
// ==============================================================================
// Use MutationObserver to detect when comparison section is shown
document.addEventListener('DOMContentLoaded', () => {
const comparisonSection = document.getElementById('comparisonSection');
if (comparisonSection) {
// Watch for style changes (display: block means conversion completed)
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
if (comparisonSection.style.display === 'block') {
// Conversion completed - show success toast
const statsDiv = comparisonSection.querySelector('.size-reduction');
if (statsDiv) {
const reductionText = statsDiv.textContent;
const match = reductionText.match(/(\d+\.?\d*)%/);
if (match) {
toast.success(`Conversion complete! Reduced by ${match[1]}%`);
}
}
// Show copy button if it exists
const copyResultsBtn = document.getElementById('copyResultsBtn');
if (copyResultsBtn) {
copyResultsBtn.style.display = 'inline-block';
}
}
}
});
});
observer.observe(comparisonSection, { attributes: true });
}
});
// ==============================================================================
// MONITOR FOR VIDEO INFO DISPLAY
// ==============================================================================
// Watch for video info section to be populated
document.addEventListener('DOMContentLoaded', () => {
const videoInfoDiv = document.getElementById('videoInfo');
if (videoInfoDiv) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Video info was populated - show copy button
const copyBtn = document.getElementById('copyVideoSpecsBtn');
if (copyBtn) {
copyBtn.style.display = 'inline-block';
}
}
});
});
observer.observe(videoInfoDiv, { childList: true });
}
});
// ==============================================================================
// ADD KEYBOARD SHORTCUT HINTS
// ==============================================================================
// Add keyboard shortcut hints to the UI
function addKeyboardShortcutHints() {
const footer = document.querySelector('footer');
if (footer) {
const hintsDiv = document.createElement('div');
hintsDiv.style.cssText = 'margin-top: 20px; padding: 15px; background: rgba(255,196,7,0.1); border-radius: 6px; font-size: 12px; color: #999;';
hintsDiv.innerHTML = `
<strong style="color: #FFC407;">Keyboard Shortcuts:</strong>
<span style="margin-left: 15px;">ESC</span> - Start over
<span style="margin-left: 15px;">Ctrl+Enter</span> - Convert
<span style="margin-left: 15px;">Ctrl+K</span> - Help
`;
footer.insertBefore(hintsDiv, footer.firstChild);
}
}
// Add hints after page load
setTimeout(addKeyboardShortcutHints, 1000);
// ==============================================================================
// WELCOME MESSAGE
// ==============================================================================
// Show welcome message on first visit
setTimeout(() => {
const hasSeenWelcome = localStorage.getItem('hasSeenWelcome');
if (!hasSeenWelcome) {
toast.info('👋 Welcome! Upload a video to get started. Press Ctrl+K for help.', 6000);
localStorage.setItem('hasSeenWelcome', 'true');
}
}, 2000);
console.log('✨ App enhancements initialized successfully!');

View file

@ -1,5 +1,6 @@
// Video Optimizer Frontend JavaScript
// API Configuration is loaded from config.js (API_BASE is available globally)
// API Configuration (imported from config.js)
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
// State
let currentFileId = null;
@ -258,9 +259,6 @@ async function handleConvert() {
progressFill.style.width = '50%';
progressText.textContent = 'Converting video...';
// Get user email for logging (frontend-only auth)
const userEmail = getUserEmail() || 'anonymous@local';
const response = await fetch(`${API_BASE}/convert`, {
method: 'POST',
headers: {
@ -270,8 +268,7 @@ async function handleConvert() {
file_id: currentFileId,
platform: platformKey,
aspect_ratio: aspectRatio,
custom_bitrate: customBitrate,
user_email: userEmail
custom_bitrate: customBitrate
})
});

View file

@ -1,171 +0,0 @@
/**
* Microsoft Authentication Library (MSAL) wrapper for SSO authentication
* Provides authentication functions for the video optimizer application
*/
let msalInstance = null;
let msalConfig = null;
const loginRequest = {
scopes: ["User.Read"]
};
/**
* Initialize MSAL with configuration from backend
* @param {Object} config - Azure AD configuration from /api/config
*/
async function initAuth(config) {
if (!config || !config.AZURE_CLIENT_ID || !config.AZURE_TENANT_ID) {
throw new Error('Invalid Azure AD configuration');
}
msalConfig = {
auth: {
clientId: config.AZURE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${config.AZURE_TENANT_ID}`,
redirectUri: config.REDIRECT_URI || window.location.origin
},
cache: {
cacheLocation: "sessionStorage", // Re-login required after browser close
storeAuthStateInCookie: false
}
};
// Initialize MSAL instance
msalInstance = new msal.PublicClientApplication(msalConfig);
// Initialize the MSAL instance (required in MSAL v3.x)
await msalInstance.initialize();
// Handle redirect promise
try {
const response = await msalInstance.handleRedirectPromise();
if (response) {
console.log('Authentication successful:', response);
}
} catch (error) {
console.error('Authentication error:', error);
// Check if it's an unauthorized user error (not in tenant)
if (error.errorCode === 'user_cancelled' ||
error.errorCode === 'access_denied' ||
error.errorMessage?.includes('AADSTS50020')) {
throw new Error('Unauthorized: You are not authorized to access this application. Please contact your administrator.');
}
throw error;
}
}
/**
* Check if user is authenticated
* @returns {boolean}
*/
function isAuthenticated() {
if (!msalInstance) {
return false;
}
const accounts = msalInstance.getAllAccounts();
return accounts.length > 0;
}
/**
* Get the current user account
* @returns {Object|null}
*/
function getAccount() {
if (!msalInstance) {
return null;
}
const accounts = msalInstance.getAllAccounts();
return accounts.length > 0 ? accounts[0] : null;
}
/**
* Get user email
* @returns {string|null}
*/
function getUserEmail() {
const account = getAccount();
return account ? (account.username || account.email || '') : null;
}
/**
* Get user display name
* @returns {string|null}
*/
function getUserName() {
const account = getAccount();
return account ? (account.name || account.username || '') : null;
}
/**
* Login with redirect
*/
async function login() {
if (!msalInstance) {
throw new Error('MSAL not initialized. Call initAuth() first.');
}
try {
await msalInstance.loginRedirect(loginRequest);
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
/**
* Logout with redirect
*/
async function logout() {
if (!msalInstance) {
throw new Error('MSAL not initialized. Call initAuth() first.');
}
try {
await msalInstance.logoutRedirect({
postLogoutRedirectUri: window.location.origin
});
} catch (error) {
console.error('Logout failed:', error);
throw error;
}
}
/**
* Get access token (if needed for API calls)
* @returns {Promise<string>}
*/
async function getAccessToken() {
if (!msalInstance) {
throw new Error('MSAL not initialized. Call initAuth() first.');
}
const account = getAccount();
if (!account) {
throw new Error('No authenticated user found');
}
try {
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: account
});
return response.accessToken;
} catch (error) {
console.error('Token acquisition failed:', error);
// If silent token acquisition fails, redirect to login
await msalInstance.acquireTokenRedirect(loginRequest);
}
}
// Export functions to window object for global access
window.initAuth = initAuth;
window.isAuthenticated = isAuthenticated;
window.getAccount = getAccount;
window.getUserEmail = getUserEmail;
window.getUserName = getUserName;
window.login = login;
window.logout = logout;
window.getAccessToken = getAccessToken;

View file

@ -1,35 +1,17 @@
// Configuration file for Video Optimizer
// Automatically detects environment (local development vs production)
// Detect environment based on hostname
const isLocalhost = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname === '';
// Set API base URL based on environment
const API_BASE = isLocalhost
? 'http://localhost:5000/api' // Local development
: `${window.location.origin}/video-optimizer/api`; // Production (relative to current domain)
// Update this based on your setup
const CONFIG = {
// Backend API URL (auto-detected based on environment)
API_BASE: API_BASE,
// Python Flask backend URL (keep running in separate terminal)
API_BASE: 'http://localhost:5000/api',
// Maximum file size in bytes (500MB default)
MAX_FILE_SIZE: 500 * 1024 * 1024,
// Enable debug logging (only in development)
DEBUG: isLocalhost,
// Is production environment
IS_PRODUCTION: !isLocalhost
// Enable debug logging
DEBUG: true
};
// Log configuration on load (only in development)
if (CONFIG.DEBUG) {
console.log('Video Optimizer Configuration:', CONFIG);
}
// Export for use in app.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = CONFIG;

View file

@ -1,391 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help - Video Optimizer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<style>
.help-content {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.help-section {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.help-section h2 {
color: #FFC407;
font-size: 24px;
margin-top: 0;
margin-bottom: 20px;
border-bottom: 2px solid #FFC407;
padding-bottom: 10px;
}
.help-section h3 {
color: #fff;
font-size: 18px;
margin-top: 25px;
margin-bottom: 15px;
}
.help-section p,
.help-section li {
line-height: 1.8;
color: #ccc;
}
.help-section ul,
.help-section ol {
margin-left: 20px;
}
.help-section li {
margin-bottom: 10px;
}
.help-section code {
background: #000;
color: #FFC407;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.help-section .example {
background: #000;
border-left: 4px solid #FFC407;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.help-section .warning {
background: #2a1a00;
border-left: 4px solid #ff6b00;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.help-section .tip {
background: #001a2a;
border-left: 4px solid #0099ff;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.nav-buttons {
display: flex;
gap: 15px;
margin-bottom: 30px;
}
.nav-buttons a,
.nav-buttons button {
padding: 12px 24px;
background: #333;
color: #fff;
text-decoration: none;
border-radius: 6px;
border: 1px solid #555;
transition: all 0.3s;
cursor: pointer;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
}
.nav-buttons a:hover,
.nav-buttons button:hover {
background: #FFC407;
color: #000;
border-color: #FFC407;
}
.platforms-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.platform-card {
background: #000;
padding: 15px;
border-radius: 6px;
border: 1px solid #333;
}
.platform-card h4 {
color: #FFC407;
margin: 0 0 10px 0;
font-size: 16px;
}
.platform-card p {
margin: 5px 0;
font-size: 13px;
}
</style>
</head>
<body>
<!-- Main Content -->
<div class="container" id="mainApp">
<!-- Header -->
<header class="header">
<h1>Help & Documentation</h1>
<p class="subtitle">Video Optimizer User Guide</p>
</header>
<!-- Navigation Buttons -->
<div class="nav-buttons">
<a href="index.html">← Back to App</a>
</div>
<div class="help-content">
<!-- Quick Start -->
<div class="help-section">
<h2>Quick Start Guide</h2>
<p>Get started with video optimization in 3 simple steps:</p>
<ol>
<li><strong>Upload Your Video</strong>
<ul>
<li>Drag and drop your video file onto the upload area</li>
<li>Or click the upload area to browse your files</li>
<li>Supported formats: MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V</li>
<li>Maximum file size: 500MB</li>
</ul>
</li>
<li><strong>Configure Platform & Aspect Ratio</strong>
<ul>
<li>Select your target platform (TikTok, Meta, YouTube, etc.)</li>
<li>Choose the aspect ratio (9:16 vertical, 16:9 horizontal, 1:1 square)</li>
<li>The app will auto-detect these from your filename if it matches the naming convention</li>
</ul>
</li>
<li><strong>Convert & Download</strong>
<ul>
<li>Click "Convert Video" to optimize</li>
<li>Compare the original and optimized videos side-by-side</li>
<li>Download the optimized version</li>
</ul>
</li>
</ol>
</div>
<!-- Smart Detection -->
<div class="help-section">
<h2>Smart Detection (Filename Patterns)</h2>
<p>The app can automatically detect the platform and aspect ratio from your filename. Use these patterns:</p>
<h3>Platform Patterns</h3>
<div class="example">
<p><code>_tiktok_</code> → TikTok</p>
<p><code>_meta_</code> or <code>_facebook_</code> or <code>_instagram_</code> → Meta (Facebook & Instagram)</p>
<p><code>_pinterest_</code> → Pinterest</p>
<p><code>_snapchat_</code> or <code>_snap_</code> → Snapchat</p>
<p><code>_youtube_</code> → YouTube</p>
<p><code>_youtube_ctv_</code> → YouTube Connected TV</p>
<p><code>_amazon_prime_</code> → Amazon Prime Video</p>
<p><code>_amazon_freevee_</code> or <code>_freevee_</code> → Amazon Freevee</p>
</div>
<h3>Aspect Ratio Patterns</h3>
<div class="example">
<p><code>_16x9_</code> or <code>_16:9_</code> or <code>_horizontal_</code> → 16:9 (Landscape)</p>
<p><code>_9x16_</code> or <code>_9:16_</code> or <code>_vertical_</code> → 9:16 (Portrait)</p>
<p><code>_1x1_</code> or <code>_1:1_</code> or <code>_square_</code> → 1:1 (Square)</p>
<p><code>_4x5_</code> or <code>_4:5_</code> → 4:5</p>
<p><code>_2x3_</code> or <code>_2:3_</code> → 2:3</p>
</div>
<h3>Example Filenames</h3>
<div class="tip">
<p><code>summer_campaign_tiktok_9x16.mp4</code> → Auto-detects TikTok + 9:16</p>
<p><code>product_launch_meta_1x1.mov</code> → Auto-detects Meta + 1:1</p>
<p><code>tutorial_youtube_16x9.mp4</code> → Auto-detects YouTube + 16:9</p>
</div>
</div>
<!-- Supported Platforms -->
<div class="help-section">
<h2>Supported Platforms</h2>
<p>The Video Optimizer supports 21 platform configurations across 8 major social media platforms:</p>
<div class="platforms-grid">
<div class="platform-card">
<h4>TikTok</h4>
<p>Codec: H264</p>
<p>Ratios: 9:16, 1:1</p>
</div>
<div class="platform-card">
<h4>Meta (FB/IG)</h4>
<p>Codec: H264</p>
<p>Ratios: 9:16, 1:1, 16:9, 4:5</p>
</div>
<div class="platform-card">
<h4>Pinterest</h4>
<p>Codec: H264</p>
<p>Ratios: 2:3, 1:1, 9:16, 16:9</p>
</div>
<div class="platform-card">
<h4>Snapchat</h4>
<p>Codec: H264</p>
<p>Ratios: 9:16, 16:9</p>
</div>
<div class="platform-card">
<h4>YouTube</h4>
<p>Codec: H264</p>
<p>Ratios: 16:9, 9:16, 1:1</p>
</div>
<div class="platform-card">
<h4>YouTube CTV</h4>
<p>Codec: H264</p>
<p>Ratios: 16:9</p>
</div>
<div class="platform-card">
<h4>Amazon Prime</h4>
<p>Codec: H264</p>
<p>Ratios: 16:9, 1:1</p>
</div>
<div class="platform-card">
<h4>Amazon Freevee</h4>
<p>Codec: H264</p>
<p>Ratios: 16:9, 1:1</p>
</div>
</div>
</div>
<!-- Admin Panel -->
<div class="help-section">
<h2>Admin Panel Access</h2>
<p>All authenticated users have access to the admin panel where you can:</p>
<ul>
<li><strong>View Platform Specifications:</strong> See all configured platforms and their settings</li>
<li><strong>Add New Platforms:</strong> Create custom platform configurations</li>
<li><strong>Edit Existing Platforms:</strong> Modify codec, bitrate, resolution for any platform</li>
<li><strong>Delete Platforms:</strong> Remove platforms you no longer need</li>
<li><strong>Export/Import Specifications:</strong> Backup or share platform configurations as JSON</li>
<li><strong>Manage Naming Conventions:</strong> Add or edit filename detection patterns</li>
<li><strong>Reset to Factory Defaults:</strong> Restore original platform configurations</li>
</ul>
<div class="tip">
<p><strong>Tip:</strong> Access the admin panel by clicking the "Admin Panel" button in the navigation or by going to <code>/admin.html</code></p>
</div>
</div>
<!-- Troubleshooting -->
<div class="help-section">
<h2>Troubleshooting</h2>
<h3>Video Upload Issues</h3>
<div class="warning">
<p><strong>Problem:</strong> "File size exceeds maximum allowed"</p>
<p><strong>Solution:</strong> The maximum file size is 500MB. Compress your video before uploading or use a smaller file.</p>
</div>
<div class="warning">
<p><strong>Problem:</strong> "File type not allowed"</p>
<p><strong>Solution:</strong> Only video formats are supported: MP4, MOV, AVI, MKV, WEBM, FLV, WMV, M4V. Convert your video to a supported format.</p>
</div>
<h3>Conversion Errors</h3>
<div class="warning">
<p><strong>Problem:</strong> Conversion fails or takes too long</p>
<p><strong>Solution:</strong> This may be due to video complexity or server load. Try:</p>
<ul>
<li>Using a shorter video</li>
<li>Using a lower resolution source file</li>
<li>Trying again after a few minutes</li>
</ul>
</div>
<h3>Playback Issues</h3>
<div class="warning">
<p><strong>Problem:</strong> Videos don't play in comparison view</p>
<p><strong>Solution:</strong> Your browser may not support the video codec. Try using a modern browser like Chrome, Firefox, or Edge.</p>
</div>
<h3>Authentication Issues</h3>
<div class="warning">
<p><strong>Problem:</strong> "Unauthorized: You are not authorized to access this application"</p>
<p><strong>Solution:</strong> Your Microsoft account is not part of the authorized organization. Contact your administrator.</p>
</div>
</div>
<!-- FAQ -->
<div class="help-section">
<h2>Frequently Asked Questions</h2>
<h3>What video formats can I upload?</h3>
<p>MP4, MOV, AVI, MKV, WEBM, FLV, WMV, and M4V files up to 500MB.</p>
<h3>How long are my videos stored on the server?</h3>
<p>Videos are automatically deleted after 24 hours to save server space.</p>
<h3>Can I download the original video after optimization?</h3>
<p>Yes! Both the original and optimized videos are available for download in the comparison view.</p>
<h3>What's the difference between platforms?</h3>
<p>Each platform has specific codec, bitrate, and resolution requirements for optimal performance. The app automatically applies the correct settings.</p>
<h3>Can I customize bitrate settings?</h3>
<p>Yes! Use the "Custom Bitrate" field in the configuration section to override the platform default.</p>
<h3>Do I need an account to use this tool?</h3>
<p>Yes, you need to sign in with your Microsoft account to access the application.</p>
<h3>Who can access the admin panel?</h3>
<p>All authenticated users have access to the admin panel.</p>
<h3>Can I add my own platform configurations?</h3>
<p>Yes! Use the admin panel to add custom platforms with specific codec and quality settings.</p>
</div>
<!-- Technical Details -->
<div class="help-section">
<h2>Technical Details</h2>
<h3>Codec Settings</h3>
<ul>
<li><strong>H.264 (libx264):</strong> Preset: medium, CRF: 23, Profile: main</li>
<li><strong>H.265 (libx265):</strong> Preset: medium, CRF: 28</li>
<li><strong>VP9 (libvpx-vp9):</strong> Deadline: good, CPU-used: 2, Row-mt: 1</li>
</ul>
<h3>Audio Settings</h3>
<ul>
<li><strong>Mobile platforms:</strong> AAC codec at 128kbps</li>
<li><strong>CTV platforms:</strong> AAC codec at 192kbps</li>
<li><strong>VP9 videos:</strong> Opus codec at 128-192kbps</li>
</ul>
<h3>File Retention</h3>
<p>Uploaded and converted files are automatically deleted after the configured retention period (default: 24 hours) to prevent server storage exhaustion.</p>
<h3>Conversion Logging</h3>
<p>All conversions are logged with metadata including user, platform, aspect ratio, file sizes, conversion duration, and status for audit and analytics purposes.</p>
</div>
<!-- Contact -->
<div class="help-section">
<h2>Need More Help?</h2>
<p>If you're experiencing issues not covered in this guide, please contact your system administrator or technical support team.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -8,39 +8,9 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<!-- MSAL Browser Library for Microsoft SSO -->
<script src="https://cdn.jsdelivr.net/npm/@azure/msal-browser@3.5.0/lib/msal-browser.min.js"></script>
</head>
<body>
<!-- Login Page (shown when not authenticated) -->
<div id="loginPage" class="login-page" style="display: none;">
<div class="login-container">
<div class="login-card">
<h1>Video Optimizer</h1>
<p class="subtitle">Social Media Platform Optimization Tool</p>
<p class="login-message">Please sign in with your Microsoft account to continue</p>
<button class="btn-login" id="loginBtn">
<span>Sign in with Microsoft</span>
</button>
<div id="loginError" class="error-message" style="display: none;"></div>
</div>
</div>
</div>
<!-- Main Application (shown when authenticated) -->
<div class="container" id="mainApp" style="display: none;">
<!-- Auth Header -->
<div class="auth-header">
<div class="auth-user-info">
<span class="user-email" id="userEmail"></span>
</div>
<div class="auth-actions">
<a href="help.html" class="btn-help" title="Help & Documentation">Help</a>
<a href="admin.html" class="btn-admin" title="Admin Panel">Admin</a>
<button class="btn-logout" id="logoutBtn">Logout</button>
</div>
</div>
<div class="container">
<!-- Header -->
<header class="header">
<h1>Video Optimizer</h1>
@ -177,74 +147,6 @@
</div>
<script src="config.js"></script>
<script src="auth.js"></script>
<script src="toast.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
<script src="app-enhancements.js"></script>
<script>
// Initialize authentication and check login status
async function initializeApp() {
try {
// Fetch Azure AD configuration from backend
const response = await fetch(`${CONFIG.API_BASE}/config`);
const config = await response.json();
// Initialize MSAL
await window.initAuth(config);
// Check authentication status
if (window.isAuthenticated()) {
// User is authenticated - show main app
const userEmail = window.getUserEmail();
document.getElementById('userEmail').textContent = userEmail || 'User';
document.getElementById('mainApp').style.display = 'block';
document.getElementById('loginPage').style.display = 'none';
} else {
// User not authenticated - show login page
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
}
} catch (error) {
console.error('Initialization error:', error);
// Show error on login page
const loginError = document.getElementById('loginError');
if (loginError) {
loginError.textContent = error.message || 'Failed to initialize authentication. Please try again.';
loginError.style.display = 'block';
}
// Show login page even on error
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('mainApp').style.display = 'none';
}
}
// Login button handler
document.getElementById('loginBtn').addEventListener('click', async () => {
try {
document.getElementById('loginError').style.display = 'none';
await window.login();
} catch (error) {
console.error('Login error:', error);
const loginError = document.getElementById('loginError');
loginError.textContent = error.message || 'Login failed. Please try again.';
loginError.style.display = 'block';
}
});
// Logout button handler
document.getElementById('logoutBtn').addEventListener('click', async () => {
try {
await window.logout();
} catch (error) {
console.error('Logout error:', error);
}
});
// Initialize app on page load
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>

View file

@ -1,6 +0,0 @@
{
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -530,165 +530,3 @@ body {
.loading {
animation: pulse 1.5s ease-in-out infinite;
}
/* ========================================
Microsoft SSO Authentication Styles
======================================== */
/* Login Page */
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: var(--primary-black);
padding: 2rem;
}
.login-container {
width: 100%;
max-width: 500px;
}
.login-card {
background-color: var(--secondary-black);
border: 2px solid var(--primary-yellow);
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
box-shadow: 0 4px 20px rgba(255, 196, 7, 0.2);
}
.login-card h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-yellow);
margin-bottom: 0.5rem;
}
.login-card .subtitle {
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 2rem;
}
.login-message {
font-size: 1.1rem;
color: var(--text-primary);
margin-bottom: 2rem;
line-height: 1.5;
}
.btn-login {
background-color: var(--primary-yellow);
color: var(--primary-black);
font-family: 'Montserrat', sans-serif;
font-size: 1.1rem;
font-weight: 600;
padding: 1rem 2.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
max-width: 300px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-login:hover {
background-color: var(--hover-yellow);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 196, 7, 0.4);
}
.btn-login:active {
transform: translateY(0);
}
/* Auth Header */
.auth-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background-color: var(--secondary-black);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 2rem;
}
.auth-user-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-email {
color: var(--primary-yellow);
font-size: 0.95rem;
font-weight: 500;
}
.auth-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.btn-help,
.btn-admin,
.btn-home {
background-color: transparent;
color: #999;
font-family: 'Montserrat', sans-serif;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1.25rem;
border: 1px solid #555;
border-radius: 6px;
text-decoration: none;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
}
.btn-help:hover,
.btn-admin:hover,
.btn-home:hover {
background-color: var(--secondary-black);
color: var(--primary-yellow);
border-color: var(--primary-yellow);
}
.btn-logout {
background-color: transparent;
color: var(--primary-yellow);
font-family: 'Montserrat', sans-serif;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1.5rem;
border: 1px solid var(--primary-yellow);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-logout:hover {
background-color: var(--primary-yellow);
color: var(--primary-black);
}
/* Error Message */
.error-message {
background-color: #3a1a1a;
color: #ff6b6b;
border: 1px solid #ff6b6b;
border-radius: 6px;
padding: 1rem;
margin-top: 1rem;
font-size: 0.9rem;
line-height: 1.5;
}

View file

@ -1,133 +0,0 @@
/**
* Toast Notification System
* Provides non-intrusive notifications for user actions
*/
class Toast {
constructor() {
this.container = this.createContainer();
document.body.appendChild(this.container);
}
createContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
`;
return container;
}
show(message, type = 'info', duration = 4000) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: ''
};
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
toast.innerHTML = `
<div style="
background: ${colors[type]};
color: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 12px;
font-family: 'Montserrat', sans-serif;
font-size: 14px;
line-height: 1.5;
animation: slideIn 0.3s ease-out;
">
<span style="font-size: 20px; font-weight: bold;">${icons[type]}</span>
<span style="flex: 1;">${message}</span>
<button onclick="this.parentElement.parentElement.remove()" style="
background: transparent;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 0;
margin: 0;
opacity: 0.8;
transition: opacity 0.2s;
" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'">×</button>
</div>
`;
this.container.appendChild(toast);
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => toast.remove(), 300);
}, duration);
}
}
success(message, duration) {
this.show(message, 'success', duration);
}
error(message, duration) {
this.show(message, 'error', duration);
}
warning(message, duration) {
this.show(message, 'warning', duration);
}
info(message, duration) {
this.show(message, 'info', duration);
}
}
// Add animations to document
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// Create global toast instance
const toast = new Toast();

View file

@ -1,411 +0,0 @@
/**
* Utility Functions for Quality of Life Improvements
*/
// ==============================================================================
// LOCAL STORAGE MANAGEMENT
// ==============================================================================
const Storage = {
/**
* Save last used settings
*/
saveLastSettings(platform, aspectRatio) {
try {
localStorage.setItem('lastPlatform', platform);
localStorage.setItem('lastAspectRatio', aspectRatio);
} catch (e) {
console.warn('Failed to save settings to localStorage:', e);
}
},
/**
* Load last used settings
*/
getLastSettings() {
try {
return {
platform: localStorage.getItem('lastPlatform'),
aspectRatio: localStorage.getItem('lastAspectRatio')
};
} catch (e) {
console.warn('Failed to load settings from localStorage:', e);
return { platform: null, aspectRatio: null };
}
},
/**
* Clear saved settings
*/
clearSettings() {
try {
localStorage.removeItem('lastPlatform');
localStorage.removeItem('lastAspectRatio');
} catch (e) {
console.warn('Failed to clear settings from localStorage:', e);
}
}
};
// ==============================================================================
// CLIPBOARD UTILITIES
// ==============================================================================
const Clipboard = {
/**
* Copy text to clipboard with fallback
*/
async copy(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
const success = document.execCommand('copy');
document.body.removeChild(textArea);
return success;
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
return false;
}
},
/**
* Copy video specs to clipboard
*/
async copyVideoSpecs(videoInfo, platform, aspectRatio) {
const specs = `
Video Specifications:
Platform: ${platform || 'N/A'}
Aspect Ratio: ${aspectRatio || 'N/A'}
Duration: ${videoInfo.duration ? Math.round(videoInfo.duration) + 's' : 'N/A'}
Resolution: ${videoInfo.width || '?'}x${videoInfo.height || '?'}
Bitrate: ${videoInfo.bitrate ? Math.round(videoInfo.bitrate / 1000) + ' kbps' : 'N/A'}
Codec: ${videoInfo.codec || 'N/A'}
`.trim();
const success = await this.copy(specs);
if (success) {
toast.success('Video specs copied to clipboard!');
} else {
toast.error('Failed to copy to clipboard');
}
},
/**
* Copy conversion results to clipboard
*/
async copyConversionResults(results) {
const text = `
Conversion Results:
Input Size: ${formatFileSize(results.input_size)}
Output Size: ${formatFileSize(results.output_size)}
Size Reduction: ${results.size_reduction_percent}%
Space Saved: ${formatFileSize(results.input_size - results.output_size)}
`.trim();
const success = await this.copy(text);
if (success) {
toast.success('Conversion results copied to clipboard!');
} else {
toast.error('Failed to copy to clipboard');
}
}
};
// ==============================================================================
// KEYBOARD SHORTCUTS
// ==============================================================================
const KeyboardShortcuts = {
init() {
document.addEventListener('keydown', (e) => {
// ESC - Clear/Reset
if (e.key === 'Escape') {
this.handleEscape();
}
// Ctrl/Cmd + Enter - Convert (if ready)
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
this.handleConvert();
}
// Ctrl/Cmd + K - Open help
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
window.location.href = 'help.html';
}
});
},
handleEscape() {
// Close modals, clear selections, etc.
const configSection = document.getElementById('configSection');
const comparisonSection = document.getElementById('comparisonSection');
const uploadSection = document.getElementById('uploadSection');
if (comparisonSection && comparisonSection.style.display !== 'none') {
// Go back to upload
if (confirm('Start over with a new video?')) {
comparisonSection.style.display = 'none';
if (configSection) configSection.style.display = 'none';
if (uploadSection) uploadSection.style.display = 'block';
window.currentFileId = null;
toast.info('Ready for new upload');
}
} else if (configSection && configSection.style.display !== 'none') {
// Go back to upload
if (confirm('Cancel and start over?')) {
configSection.style.display = 'none';
if (uploadSection) uploadSection.style.display = 'block';
window.currentFileId = null;
toast.info('Upload cancelled');
}
}
},
handleConvert() {
const convertBtn = document.getElementById('convertBtn');
if (convertBtn && !convertBtn.disabled) {
convertBtn.click();
toast.info('Starting conversion... (Ctrl+Enter)');
}
}
};
// ==============================================================================
// FILE SIZE FORMATTING
// ==============================================================================
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
// ==============================================================================
// TIME ESTIMATION
// ==============================================================================
function estimateConversionTime(fileSize, platform) {
// Rough estimates based on file size (in MB)
const sizeInMB = fileSize / (1024 * 1024);
// Base processing time: ~2-4 seconds per MB for H264
const baseTime = sizeInMB * 3;
// Add overhead for different platforms
const overhead = 5; // seconds
const totalSeconds = Math.max(10, baseTime + overhead);
if (totalSeconds < 60) {
return `~${Math.round(totalSeconds)} seconds`;
} else {
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `~${minutes}m ${seconds}s`;
}
}
// ==============================================================================
// DRAG & DROP IMPROVEMENTS
// ==============================================================================
const DragDropEnhancer = {
init(dropZoneElement) {
if (!dropZoneElement) return;
let dragCounter = 0;
dropZoneElement.addEventListener('dragenter', (e) => {
e.preventDefault();
dragCounter++;
if (dragCounter === 1) {
dropZoneElement.classList.add('drag-over');
dropZoneElement.style.borderColor = '#FFC407';
dropZoneElement.style.background = 'rgba(255, 196, 7, 0.1)';
}
});
dropZoneElement.addEventListener('dragleave', (e) => {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
dropZoneElement.classList.remove('drag-over');
dropZoneElement.style.borderColor = '';
dropZoneElement.style.background = '';
}
});
dropZoneElement.addEventListener('dragover', (e) => {
e.preventDefault();
});
dropZoneElement.addEventListener('drop', (e) => {
e.preventDefault();
dragCounter = 0;
dropZoneElement.classList.remove('drag-over');
dropZoneElement.style.borderColor = '';
dropZoneElement.style.background = '';
});
}
};
// ==============================================================================
// BETTER ERROR MESSAGES
// ==============================================================================
const ErrorHandler = {
getHelpfulErrorMessage(error) {
const errorStr = error.toString().toLowerCase();
if (errorStr.includes('network') || errorStr.includes('fetch')) {
return {
message: 'Network Error',
details: 'Cannot connect to the server. Please check your internet connection.',
action: 'Try refreshing the page or contact your administrator.'
};
}
if (errorStr.includes('timeout')) {
return {
message: 'Request Timeout',
details: 'The operation took too long to complete.',
action: 'Try with a smaller video file or try again later.'
};
}
if (errorStr.includes('file size') || errorStr.includes('too large')) {
return {
message: 'File Too Large',
details: 'The video file exceeds the maximum allowed size (500MB).',
action: 'Compress your video before uploading or use a smaller file.'
};
}
if (errorStr.includes('unauthorized') || errorStr.includes('403')) {
return {
message: 'Access Denied',
details: 'You do not have permission to perform this action.',
action: 'Please sign in again or contact your administrator.'
};
}
if (errorStr.includes('ffmpeg') || errorStr.includes('codec')) {
return {
message: 'Conversion Error',
details: 'The video conversion failed due to encoding issues.',
action: 'Try a different video format or contact support.'
};
}
return {
message: 'Error',
details: error.toString(),
action: 'Please try again or contact support if the problem persists.'
};
},
show(error) {
const helpful = this.getHelpfulErrorMessage(error);
toast.error(`${helpful.message}: ${helpful.details}\n${helpful.action}`, 8000);
}
};
// ==============================================================================
// LOADING STATE IMPROVEMENTS
// ==============================================================================
const LoadingState = {
show(message = 'Processing...') {
let loader = document.getElementById('global-loader');
if (!loader) {
loader = document.createElement('div');
loader.id = 'global-loader';
loader.innerHTML = `
<div style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
">
<div style="
width: 60px;
height: 60px;
border: 4px solid #333;
border-top-color: #FFC407;
border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
<p id="loader-message" style="
color: #FFC407;
font-family: 'Montserrat', sans-serif;
font-size: 18px;
margin-top: 20px;
">${message}</p>
</div>
`;
document.body.appendChild(loader);
// Add spin animation
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
} else {
loader.style.display = 'flex';
const messageEl = document.getElementById('loader-message');
if (messageEl) messageEl.textContent = message;
}
},
hide() {
const loader = document.getElementById('global-loader');
if (loader) {
loader.style.display = 'none';
}
},
updateMessage(message) {
const messageEl = document.getElementById('loader-message');
if (messageEl) messageEl.textContent = message;
}
};
// ==============================================================================
// EXPORT UTILITIES
// ==============================================================================
// Make utilities globally available
window.Storage = Storage;
window.Clipboard = Clipboard;
window.KeyboardShortcuts = KeyboardShortcuts;
window.formatFileSize = formatFileSize;
window.estimateConversionTime = estimateConversionTime;
window.DragDropEnhancer = DragDropEnhancer;
window.ErrorHandler = ErrorHandler;
window.LoadingState = LoadingState;

View file

@ -1,12 +0,0 @@
{
"boxAppSettings": {
"clientID": "yn5pau5gd3f00e84e2kcudfrl1ekn8hn",
"clientSecret": "tUurcI4oZetmWtQJtlG6ZT3E6mjeW1CE",
"appAuth": {
"publicKeyID": "qxbaongc",
"privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQtpKg4auw7lG20N+1\n09Ul1wICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEO4JqHHpjHLamlnq\nAVg7ScEEggTQldAR75JIR8ISefH2T6ioxQMra0CZ/y83G7SvGxiuCtNM9U3lEXNP\nU9MeXxqLwkvBwgEZ3G9fD+yWyQP7vnhbUK0EF+8DXAzdLVkM7gOpw/ajI7qiRT2m\nM1wXVLQAOvytZFbb2QZbq+t1+hE4UZDYXoKo2P/cV1ryJCwfaCciMQQQjz2MygV8\nhl1xXQDEewA8m7c150V0ky/J3EVaPeHUOC26c0NxxkF8VyLZIuGv1PLlVWSpsBgr\nqTX34kUPEHM7aNHhIf3Ig3jU+VAcPBl26FGWHr9edozZd509CHWwaa/WI0w4nGo0\nTAQLZAkvu85ubD60DqdR4KG9Nt7d9eHjrMMmHSep04eFyOH09dCMf0H5ds5ic0o7\nv9pYGFehV0l9Hi/2VViMUAV6psBa+KFVLd4tFyBr3cWNUlbS6Eldy8zlApoGyQWE\nHi1rtAxwsxMvvTbXUCCl4RKYV9W3Equx9zGt6nJQpS0s25Pbz3dfSQMdxq12iVuR\n5FGNtHiaIAUJgAapfbYFu4B9x+Z4247oOBhcodu1IYJf0NhSUldPSJjYE3uJ3G0z\n5BY9AU5QSBuTCdtosHSLaSJdCabpgPaHDiZhjAyVoh+uVUFifWj762hMNV2IXtTc\n1Q5U3LcLRm5S+ww2FjT2qKV0bMKUJuTeDF1FP56Q4fUiFZvJ512uCNl8G1ZHVPeS\n3HAaaizyLGFEPvLVn0yyUJqysgnfjqudbTuRWheypPe6o2m6EMJ2uIHbASFV0sdh\nnErxMyzlz+qSjFAjcsaIRaZLqAnURIAte+fCPa6gdSG6TvOnc7tZBi9UVHyAaxgp\nycPDooQdv1+cucut0YKSyt1Aqcmy/+ZqDSiPgzNznjqdSQVyqf8ylKOqAqwcmJpu\nn7z0qP6xuua5csMqcCFJ/FQfWThXcbSYAG7fH0MnceFVikDJohkMk+bFlSAiKnVL\nAs7LtvLDeBIIkulf8eSKro130lr+CZnbMViYSH57w9uMQsSoMfuNB0ABEWjjQTxh\n7Y/EcphXHDcfcqW+PDJOZ3b55Fz+tJZ2nTifHcbrnMZdTWB7rvI3SYb4Tmh7Chqg\nf8MfKeKvv55s8b54z9/fL18EmfUAfSBQg5S0W8eCCJUWeBgPG1uAaZWR6RVy7bSY\nWGrHLCI/BAW+dEbISmIj5wJC8xZYssLsbubSn///iwSzLwfRvZg+0WI5LZpFbv7o\nhztM5M/ZUavZ2AoJIt0llso/9iyOofwiV0J4YMGvP52IfkB535rzFsGldqdtEoeL\nis2V3bhFbWMr6jdx9TEX/3AbOh/icXNca7ebguxiMw4WPn9TZ9R53Bxihy3uQV58\n1fkZL24wOQDINmJ/Yk1goUO7C0khxHGHbyFrR5C61VqQjjzlIPPcInec82XWWntw\n1Bo12jfPwFyJnMe0Td1zSCMvfVX5RY5mqHK5XYPE+1uVH1Wv9zL6KKEcI9zV9deR\nObnt8PH8+yHmzHBAsC1Zt84FklYzk4tFst95aCfw6EYqJ5prDUeFutskaN+L1Jqt\n0G74pQhOix6+0Q0nSggzA5xruVIolWkt/BGlHVz3J/hPso3V6FfoxaiFjqVYs6Q0\nI4rI6pF4FUCWEFW/Ju26nFbqy8rwF8JK+Bs06H0kwW92u3ehf5Z5biI=\n-----END ENCRYPTED PRIVATE KEY-----\n",
"passphrase": "bf75458860619d46eb18cfac396085ec"
}
},
"enterpriseID": "43984435"
}

View file

@ -1,53 +1,23 @@
#!/bin/bash
# Video Optimizer Startup Script
# Starts both backend and frontend for local development
echo "========================================="
echo "🎬 Video Optimizer - Startup"
echo "========================================="
echo "🎬 Starting Video Optimizer..."
echo ""
# Check if .env file exists
if [ ! -f ".env" ]; then
echo "❌ .env file not found!"
echo ""
echo " Please create a .env file from .env.example:"
echo " 1. Copy the template: cp .env.example .env"
echo " 2. Edit .env and add your Azure AD credentials"
echo " 3. Run this script again"
echo ""
exit 1
fi
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo "❌ Virtual environment not found!"
echo ""
echo " Please set up the environment first:"
echo " 1. Create venv: python3 -m venv venv"
echo " 2. Activate venv: source venv/bin/activate"
echo " 3. Install dependencies: pip install -r backend/requirements.txt"
echo " 4. Run this script again"
echo ""
echo "❌ Virtual environment not found. Please run setup first:"
echo " python3 -m venv venv"
echo " source venv/bin/activate"
echo " pip install -r backend/requirements.txt"
exit 1
fi
# Check if FFmpeg is installed
if ! command -v ffmpeg &> /dev/null; then
echo "⚠️ WARNING: FFmpeg is not installed!"
echo ""
echo " Video conversion will not work without FFmpeg."
echo " Install with:"
echo " - macOS: brew install ffmpeg"
echo " - Ubuntu/Debian: sudo apt-get install ffmpeg"
echo " - Windows: Download from https://ffmpeg.org/download.html"
echo ""
read -p " Continue anyway? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
echo " Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)"
echo ""
fi
@ -55,63 +25,32 @@ fi
echo "🔧 Activating virtual environment..."
source venv/bin/activate
# Load environment variables to display configuration
source .env 2>/dev/null
# Display configuration
echo ""
echo "📋 Configuration:"
echo " Environment: ${FLASK_ENV:-development}"
echo " Backend Port: ${BACKEND_PORT:-5000}"
echo " File Retention: ${FILE_RETENTION_HOURS:-24} hours"
echo " Max File Size: ${MAX_FILE_SIZE_MB:-500}MB"
echo ""
# Start backend server
echo "🚀 Starting backend server..."
echo "🚀 Starting backend server on http://localhost:5000..."
cd backend
python app.py &
BACKEND_PID=$!
cd ..
# Wait for backend to start
echo " Waiting for backend to initialize..."
sleep 3
# Check if backend started successfully
if ! kill -0 $BACKEND_PID 2>/dev/null; then
echo ""
echo "❌ Backend failed to start!"
echo " Check backend/logs for errors"
exit 1
fi
# Start frontend server
echo "🌐 Starting frontend server..."
cd frontend
python3 -m http.server 3000 > /dev/null 2>&1 &
echo "🌐 Starting frontend server on http://localhost:8000..."
cd ../frontend
python3 -m http.server 8000 &
FRONTEND_PID=$!
cd ..
# Wait for frontend to start
sleep 1
echo ""
echo "========================================="
echo "✅ Application is running!"
echo "========================================="
echo ""
echo " Backend: http://localhost:${BACKEND_PORT:-5000}"
echo " Frontend: http://localhost:3000"
echo " Backend: http://localhost:5000"
echo " Frontend: http://localhost:8000"
echo ""
echo " 👉 Open your browser: http://localhost:3000"
echo " 📚 Help page: http://localhost:3000/help.html"
echo " ⚙️ Admin panel: http://localhost:3000/admin.html"
echo " Open your browser and navigate to http://localhost:8000"
echo ""
echo " Press Ctrl+C to stop all servers"
echo "========================================="
echo ""
# Wait for Ctrl+C
trap "echo ''; echo '🛑 Stopping servers...'; kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; echo '✅ Servers stopped'; exit" INT
trap "echo ''; echo '🛑 Stopping servers...'; kill $BACKEND_PID $FRONTEND_PID; exit" INT
wait