Compare commits
8 commits
main
...
feature-br
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f7a9d958 | ||
|
|
fcf08193c6 | ||
|
|
113684e7b3 | ||
|
|
74b01ee69d | ||
|
|
aae99c42cb | ||
|
|
77274a7540 | ||
|
|
6dd935b933 | ||
|
|
eb3580f177 |
58 changed files with 9043 additions and 2384 deletions
35
.env.development
Normal file
35
.env.development
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ==============================================================================
|
||||
# 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=
|
||||
45
.env.example
Normal file
45
.env.example
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
37
.env.production
Normal file
37
.env.production
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# ==============================================================================
|
||||
# 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
13
.gitignore
vendored
|
|
@ -14,14 +14,20 @@ 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
|
||||
|
|
@ -41,6 +47,10 @@ Thumbs.db
|
|||
|
||||
# Logs
|
||||
*.log
|
||||
*.pdf
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
|
@ -57,3 +67,4 @@ target/
|
|||
|
||||
# Unit test reports
|
||||
TEST*.xml
|
||||
mc_box_config.json
|
||||
|
|
|
|||
1
.htaccess
Normal file
1
.htaccess
Normal file
|
|
@ -0,0 +1 @@
|
|||
Access all Denied
|
||||
340
ADMIN_GUIDE.md
340
ADMIN_GUIDE.md
|
|
@ -1,340 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# 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
Normal file
410
CLAUDE.md
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
# 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
|
||||
1409
DEPLOYMENT.md
Normal file
1409
DEPLOYMENT.md
Normal file
File diff suppressed because it is too large
Load diff
390
DEPLOYMENT_phase2.md
Normal file
390
DEPLOYMENT_phase2.md
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# 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
|
||||
```
|
||||
828
ENVIRONMENT_SETUP.md
Normal file
828
ENVIRONMENT_SETUP.md
Normal file
|
|
@ -0,0 +1,828 @@
|
|||
# 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
|
||||
Binary file not shown.
374
MAMP_SETUP.md
374
MAMP_SETUP.md
|
|
@ -1,374 +0,0 @@
|
|||
# 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!
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
# 🎉 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
274
QUICKSTART.md
|
|
@ -1,274 +0,0 @@
|
|||
# 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
|
||||
874
README.md
874
README.md
|
|
@ -1,539 +1,461 @@
|
|||
# Video Optimizer for Social Media Platforms
|
||||
|
||||
L'Oréal Creative Optimization Tool - Based on L'Oréal CDMO Creative Optimization Documentation v1.1
|
||||
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.
|
||||
|
||||
## Overview
|
||||
---
|
||||
|
||||
This application optimizes video files for various social media platforms using platform-specific codecs and bitrate recommendations to minimize file sizes while maintaining quality.
|
||||
## Modes of Operation
|
||||
|
||||
### 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
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| 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 |
|
||||
| Platform | Codec | Aspect Ratios | Bitrate Range |
|
||||
|----------|-------|---------------|---------------|
|
||||
| **Meta (Facebook/Instagram)** | H264 | 1:1, 16:9, 4:5, 9:16 | 840–1400 kbps |
|
||||
| **Pinterest** | H264 | 1:1, 16:9, 2:3, 4:5, 9:16 | 1100–1690 kbps |
|
||||
| **Snapchat** | H264 | 16:9, 9:16 | 1100–1400 kbps |
|
||||
| **TikTok** | H265 | 1:1, 16:9, 9:16 | 840–1300 kbps |
|
||||
| **YouTube & DV360** | VP9 | 1:1, 16:9, 4:5, 9:16 | 1300–2000 kbps |
|
||||
| **YouTube CTV** | VP9 | 16:9 | 3300–7000 kbps |
|
||||
| **Amazon Prime** | H264 | 16:9 | 15000 kbps fixed |
|
||||
| **Amazon Freevee** | H264 | 16:9 | 4500–7000 kbps |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Prerequisites
|
||||
|
||||
### Prerequisites
|
||||
- **Python 3.8+**
|
||||
- **FFmpeg** — required for all video processing
|
||||
|
||||
1. **Python 3.8+**
|
||||
2. **FFmpeg** - Required for video processing
|
||||
|
||||
#### Install FFmpeg
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Standard Setup)
|
||||
|
||||
### Option 1: One-Command Start
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
This starts both backend (port 5000) and frontend (port 8000) automatically.
|
||||
|
||||
### 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**
|
||||
|
||||
---
|
||||
|
||||
## MAMP Setup
|
||||
|
||||
For users who prefer MAMP for frontend hosting:
|
||||
|
||||
### 1. Start Python Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
source ../venv/bin/activate
|
||||
python app.py
|
||||
```
|
||||
|
||||
**Keep this terminal running!** Backend must be active on port 5000.
|
||||
|
||||
### 2. Configure MAMP
|
||||
|
||||
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**
|
||||
|
||||
### 3. Access Application
|
||||
|
||||
Navigate to: **http://localhost:8888/index.html**
|
||||
|
||||
(Or use your MAMP port if different)
|
||||
|
||||
**See MAMP_SETUP.md for detailed MAMP configuration and troubleshooting.**
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
|
||||
### 1. Upload Video
|
||||
|
||||
- **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)
|
||||
|
||||
### 2. Automatic Detection
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## Features in Detail
|
||||
|
||||
### Aspect Ratio Mismatch Warnings
|
||||
|
||||
**Configuration Page (Yellow Warning):**
|
||||
- Appears when selected aspect ratio differs from original
|
||||
- Warns about potential distortion before conversion
|
||||
- Helps prevent accidental aspect ratio changes
|
||||
|
||||
**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
|
||||
|
||||
### Video Specifications Display
|
||||
|
||||
Below each video player, detailed specifications are shown:
|
||||
|
||||
**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
|
||||
|
||||
**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)
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/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 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-File-Reduction/
|
||||
├── backend/
|
||||
│ ├── 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 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Codec Settings
|
||||
|
||||
**H264 (Meta, Pinterest, Snapchat, Amazon):**
|
||||
- Preset: medium
|
||||
- CRF: 23
|
||||
- Profile: main
|
||||
- Pixel format: yuv420p
|
||||
|
||||
**H265 (TikTok):**
|
||||
- Preset: medium
|
||||
- CRF: 28
|
||||
- Pixel format: yuv420p
|
||||
|
||||
**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
|
||||
|
||||
### Backend Won't Start
|
||||
|
||||
**Issue:** Port 5000 already in use
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Find and kill process on port 5000
|
||||
lsof -ti:5000 | xargs kill
|
||||
|
||||
# Or use a different port
|
||||
python app.py --port 5001 # Then update frontend/config.js
|
||||
```
|
||||
|
||||
### FFmpeg Not Found
|
||||
|
||||
**Verify installation:**
|
||||
```bash
|
||||
ffmpeg -version
|
||||
```
|
||||
|
||||
**If not installed:**
|
||||
```bash
|
||||
# macOS
|
||||
brew install ffmpeg
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install ffmpeg
|
||||
|
||||
# Windows - Download from ffmpeg.org and add to PATH
|
||||
```
|
||||
|
||||
### 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.)
|
||||
## Installation
|
||||
|
||||
### CORS Errors
|
||||
|
||||
**Verify** `backend/app.py` has:
|
||||
```python
|
||||
from flask_cors import CORS
|
||||
CORS(app)
|
||||
```bash
|
||||
# Clone and create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
pip install -r backend/requirements.txt
|
||||
```
|
||||
|
||||
Already included! If issues persist, check browser console for details.
|
||||
---
|
||||
|
||||
### Video Won't Play
|
||||
## Configuration
|
||||
|
||||
**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)
|
||||
Copy the example env file and fill in your credentials:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
## Running the Web UI
|
||||
|
||||
- **Colors:** Black (#000000) + Yellow (#FFC407)
|
||||
- **Font:** Montserrat (Google Fonts)
|
||||
- **Theme:** Dark UI with yellow accents
|
||||
- **Layout:** Responsive, mobile-friendly design
|
||||
```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.
|
||||
|
||||
Admin panel: **http://localhost:3000/admin.html**
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
## Running the Box Automation Service
|
||||
|
||||
### Expected Conversion Times
|
||||
### Step 1 — Verify setup (run once)
|
||||
|
||||
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
|
||||
python box_setup.py
|
||||
```
|
||||
|
||||
Frontend with hot reload - use your preferred tool or simple HTTP server.
|
||||
This authenticates with Box, lists accessible folders, discovers `IN` / `OUT_SUCCESS` / `OUT_FAILED` sub-folders and prints a full configuration summary.
|
||||
|
||||
### Adding New Platforms
|
||||
### Step 2 — Start the service
|
||||
|
||||
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
|
||||
Box automation runs inside the same `app.py` process as the web UI — no separate service needed.
|
||||
|
||||
**Polling mode** (recommended for development — no public URL needed):
|
||||
```bash
|
||||
# Set in .env: BOX_USE_POLLING=true
|
||||
./start.sh # or: python backend/app.py
|
||||
```
|
||||
|
||||
**Webhook mode** (for production):
|
||||
```bash
|
||||
# Expose local port via ngrok (development)
|
||||
ngrok http 5000
|
||||
|
||||
# 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`
|
||||
|
||||
### 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"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Box Naming Convention
|
||||
|
||||
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`.
|
||||
|
||||
### 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_` |
|
||||
|
||||
### Aspect Ratio Patterns
|
||||
| Ratio | Patterns |
|
||||
|-------|---------|
|
||||
| 1:1 | `_1x1_`, `_square_` |
|
||||
| 16:9 | `_16x9_`, `_landscape_` |
|
||||
| 9:16 | `_9x16_`, `_vertical_`, `_portrait_` |
|
||||
| 4:5 | `_4x5_` |
|
||||
| 2:3 | `_2x3_` |
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Box Automation Pipeline
|
||||
|
||||
When a valid file is detected in the `IN` folder:
|
||||
|
||||
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
|
||||
|
||||
On failure or invalid filename, an error report JSON is placed in `OUT_FAILED`.
|
||||
|
||||
### Box Folder Structure
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Success Report Format (`*_report.json`)
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
| 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/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 |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
loreal-video-optimizer/
|
||||
├── 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
|
||||
├── 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Codec Settings Reference
|
||||
|
||||
**H264** (Meta, Pinterest, Snapchat, Amazon):
|
||||
- `preset=medium`, `crf=23`, `profile=main`, `pix_fmt=yuv420p`
|
||||
|
||||
**H265** (TikTok):
|
||||
- `preset=medium`, `crf=28`, `pix_fmt=yuv420p`
|
||||
|
||||
**VP9** (YouTube, YouTube CTV):
|
||||
- `deadline=good`, `cpu-used=2`, `row-mt=1`
|
||||
|
||||
**Audio:**
|
||||
- AAC 128 kbps (mobile platforms)
|
||||
- AAC/Opus 192 kbps (CTV platforms)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Box automation not working
|
||||
```bash
|
||||
# Run diagnostic script
|
||||
python backend/box_setup.py
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### Port already in use
|
||||
```bash
|
||||
lsof -ti:5000 | xargs kill # Kills both web API and Box automation (same process)
|
||||
```
|
||||
|
||||
### FFmpeg not found
|
||||
```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
|
||||
|
||||
For production use:
|
||||
Services are managed via systemd on the production server:
|
||||
|
||||
1. **Use production WSGI server** (Gunicorn, uWSGI)
|
||||
```bash
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
```bash
|
||||
# Main backend
|
||||
sudo systemctl start video-optimizer-backend
|
||||
sudo systemctl enable video-optimizer-backend
|
||||
|
||||
2. **Set up reverse proxy** (Nginx, Apache)
|
||||
# Box automation service
|
||||
sudo systemctl start box-processor
|
||||
sudo systemctl enable box-processor
|
||||
|
||||
3. **Configure SSL/HTTPS**
|
||||
# View logs
|
||||
sudo journalctl -u video-optimizer-backend -f
|
||||
sudo journalctl -u box-processor -f
|
||||
```
|
||||
|
||||
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
|
||||
See `deployment/` folder for full deployment scripts and Apache configuration.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
## Design
|
||||
|
||||
- **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.
|
||||
- **Colors:** Black (`#000000`) background + Yellow (`#FFC407`) accents
|
||||
- **Font:** Montserrat (Google Fonts)
|
||||
- **Theme:** Dark UI
|
||||
|
||||
---
|
||||
|
||||
## Version
|
||||
|
||||
**Current Version:** 1.0
|
||||
**Last Updated:** October 2025
|
||||
**Current Version:** 2.0
|
||||
**Last Updated:** February 2026
|
||||
|
||||
**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
|
||||
**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
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
# 🚀 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
272
TESTING.md
|
|
@ -1,272 +0,0 @@
|
|||
# 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.
|
||||
133
backend/.htaccess
Normal file
133
backend/.htaccess
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
# ==============================================================================
|
||||
312
backend/app.py
312
backend/app.py
|
|
@ -1,15 +1,19 @@
|
|||
"""
|
||||
Flask backend for video optimization tool
|
||||
Flask backend for video optimization tool.
|
||||
Includes Box.com automation (webhook + polling) on the same port.
|
||||
"""
|
||||
|
||||
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,
|
||||
|
|
@ -21,8 +25,34 @@ 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__)
|
||||
CORS(app)
|
||||
|
||||
# ==============================================================================
|
||||
# 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)
|
||||
|
||||
# Store factory defaults (original specs from platform_specs.py)
|
||||
import copy
|
||||
|
|
@ -30,19 +60,61 @@ FACTORY_DEFAULTS = copy.deepcopy(PLATFORM_SPECS)
|
|||
FACTORY_FILENAME_PATTERNS = copy.deepcopy(FILENAME_PATTERNS)
|
||||
FACTORY_ASPECT_RATIO_PATTERNS = copy.deepcopy(ASPECT_RATIO_PATTERNS)
|
||||
|
||||
# Configuration
|
||||
# ==============================================================================
|
||||
# FILE AND FOLDER 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'}
|
||||
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
|
||||
|
||||
# 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'))
|
||||
|
||||
# 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):
|
||||
|
|
@ -61,6 +133,16 @@ 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"""
|
||||
|
|
@ -151,6 +233,7 @@ 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
|
||||
|
|
@ -164,6 +247,9 @@ 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)
|
||||
|
|
@ -174,6 +260,9 @@ 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(
|
||||
|
|
@ -183,11 +272,26 @@ 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,
|
||||
|
|
@ -199,6 +303,28 @@ 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
|
||||
|
||||
|
||||
|
|
@ -527,6 +653,148 @@ 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()
|
||||
|
|
@ -549,8 +817,36 @@ 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)")
|
||||
|
||||
print("Starting Video Optimization Server...")
|
||||
# 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(f"Upload folder: {UPLOAD_FOLDER}")
|
||||
print(f"Output folder: {OUTPUT_FOLDER}")
|
||||
print(f"Platforms: {len(PLATFORM_SPECS)} configured")
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
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
|
||||
)
|
||||
|
|
|
|||
262
backend/box_client.py
Normal file
262
backend/box_client.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"""
|
||||
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
|
||||
426
backend/box_processor.py
Normal file
426
backend/box_processor.py
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
"""
|
||||
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)
|
||||
277
backend/box_setup.py
Normal file
277
backend/box_setup.py
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
"""
|
||||
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()
|
||||
204
backend/conversion_logger.py
Normal file
204
backend/conversion_logger.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
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)
|
||||
}
|
||||
181
backend/file_cleanup.py
Normal file
181
backend/file_cleanup.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
"""
|
||||
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())
|
||||
41
backend/logs/conversions/2025-12-15_conversions.json
Normal file
41
backend/logs/conversions/2025-12-15_conversions.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
15
backend/logs/conversions/2026-02-12_conversions.json
Normal file
15
backend/logs/conversions/2026-02-12_conversions.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
67
backend/logs/conversions/2026-02-24_conversions.json
Normal file
67
backend/logs/conversions/2026-02-24_conversions.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
15
backend/logs/conversions/2026-02-25_conversions.json
Normal file
15
backend/logs/conversions/2026-02-25_conversions.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
67
backend/logs/conversions/2026-03-10_conversions.json
Normal file
67
backend/logs/conversions/2026-03-10_conversions.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
|
@ -2,3 +2,5 @@ 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
|
||||
|
|
|
|||
18
backend/run_cleanup.py
Normal file
18
backend/run_cleanup.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/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())
|
||||
114
backend/test_box_processor.py
Normal file
114
backend/test_box_processor.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
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()
|
||||
68
deployment/.env.production
Normal file
68
deployment/.env.production
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
# ==============================================================================
|
||||
114
deployment/apache-complete.conf
Normal file
114
deployment/apache-complete.conf
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
49
deployment/apache-minimal.conf
Normal file
49
deployment/apache-minimal.conf
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
40
deployment/box-processor.service
Normal file
40
deployment/box-processor.service
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# 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
|
||||
289
deployment/deploy.sh
Normal file
289
deployment/deploy.sh
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
#!/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 ""
|
||||
36
deployment/video-optimizer-backend.service
Normal file
36
deployment/video-optimizer-backend.service
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
[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
|
||||
120
frontend/.htaccess
Normal file
120
frontend/.htaccess
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
# ==============================================================================
|
||||
309
frontend/admin-enhancements.js
Normal file
309
frontend/admin-enhancements.js
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
/**
|
||||
* 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!');
|
||||
|
|
@ -1,5 +1,199 @@
|
|||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,39 @@
|
|||
<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>
|
||||
<div class="container">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>Admin Panel</h1>
|
||||
|
|
@ -95,6 +125,72 @@
|
|||
</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">
|
||||
|
|
@ -158,6 +254,74 @@
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// Admin Panel JavaScript
|
||||
// API Configuration
|
||||
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
|
||||
// API Configuration is loaded from config.js (API_BASE is available globally)
|
||||
|
||||
// State
|
||||
let platforms = [];
|
||||
|
|
@ -14,6 +13,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
loadPlatforms();
|
||||
loadNamingConventions();
|
||||
setupEventListeners();
|
||||
loadBoxStatus();
|
||||
loadBoxHistory();
|
||||
});
|
||||
|
||||
// Event Listeners
|
||||
|
|
@ -34,6 +35,16 @@ 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
|
||||
|
|
@ -656,3 +667,117 @@ 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>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
352
frontend/app-enhancements.js
Normal file
352
frontend/app-enhancements.js
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
/**
|
||||
* 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!');
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// Video Optimizer Frontend JavaScript
|
||||
// API Configuration (imported from config.js)
|
||||
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
|
||||
// API Configuration is loaded from config.js (API_BASE is available globally)
|
||||
|
||||
// State
|
||||
let currentFileId = null;
|
||||
|
|
@ -259,6 +258,9 @@ 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: {
|
||||
|
|
@ -268,7 +270,8 @@ async function handleConvert() {
|
|||
file_id: currentFileId,
|
||||
platform: platformKey,
|
||||
aspect_ratio: aspectRatio,
|
||||
custom_bitrate: customBitrate
|
||||
custom_bitrate: customBitrate,
|
||||
user_email: userEmail
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
171
frontend/auth.js
Normal file
171
frontend/auth.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* 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;
|
||||
|
|
@ -1,17 +1,35 @@
|
|||
// Configuration file for Video Optimizer
|
||||
// Update this based on your setup
|
||||
// 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)
|
||||
|
||||
const CONFIG = {
|
||||
// Python Flask backend URL (keep running in separate terminal)
|
||||
API_BASE: 'http://localhost:5000/api',
|
||||
// Backend API URL (auto-detected based on environment)
|
||||
API_BASE: API_BASE,
|
||||
|
||||
// Maximum file size in bytes (500MB default)
|
||||
MAX_FILE_SIZE: 500 * 1024 * 1024,
|
||||
|
||||
// Enable debug logging
|
||||
DEBUG: true
|
||||
// Enable debug logging (only in development)
|
||||
DEBUG: isLocalhost,
|
||||
|
||||
// Is production environment
|
||||
IS_PRODUCTION: !isLocalhost
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
391
frontend/help.html
Normal file
391
frontend/help.html
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
<!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>
|
||||
|
|
@ -8,9 +8,39 @@
|
|||
<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>
|
||||
<div class="container">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>Video Optimizer</h1>
|
||||
|
|
@ -147,6 +177,74 @@
|
|||
</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>
|
||||
|
|
|
|||
6
frontend/package-lock.json
generated
Normal file
6
frontend/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -530,3 +530,165 @@ 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;
|
||||
}
|
||||
|
|
|
|||
133
frontend/toast.js
Normal file
133
frontend/toast.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* 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();
|
||||
411
frontend/utils.js
Normal file
411
frontend/utils.js
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/**
|
||||
* 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;
|
||||
12
oliver_box_config.json
Normal file
12
oliver_box_config.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
89
start.sh
89
start.sh
|
|
@ -1,23 +1,53 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Video Optimizer Startup Script
|
||||
# Starts both backend and frontend for local development
|
||||
|
||||
echo "🎬 Starting Video Optimizer..."
|
||||
echo "========================================="
|
||||
echo "🎬 Video Optimizer - Startup"
|
||||
echo "========================================="
|
||||
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. Please run setup first:"
|
||||
echo " python3 -m venv venv"
|
||||
echo " source venv/bin/activate"
|
||||
echo " pip install -r backend/requirements.txt"
|
||||
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 ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if FFmpeg is installed
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "⚠️ WARNING: FFmpeg is not installed!"
|
||||
echo " Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)"
|
||||
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 ""
|
||||
fi
|
||||
|
||||
|
|
@ -25,32 +55,63 @@ 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 on http://localhost:5000..."
|
||||
echo "🚀 Starting backend server..."
|
||||
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 on http://localhost:8000..."
|
||||
cd ../frontend
|
||||
python3 -m http.server 8000 &
|
||||
echo "🌐 Starting frontend server..."
|
||||
cd frontend
|
||||
python3 -m http.server 3000 > /dev/null 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
cd ..
|
||||
|
||||
# Wait for frontend to start
|
||||
sleep 1
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "✅ Application is running!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo " Backend: http://localhost:5000"
|
||||
echo " Frontend: http://localhost:8000"
|
||||
echo " Backend: http://localhost:${BACKEND_PORT:-5000}"
|
||||
echo " Frontend: http://localhost:3000"
|
||||
echo ""
|
||||
echo " Open your browser and navigate to http://localhost:8000"
|
||||
echo " 👉 Open your browser: http://localhost:3000"
|
||||
echo " 📚 Help page: http://localhost:3000/help.html"
|
||||
echo " ⚙️ Admin panel: http://localhost:3000/admin.html"
|
||||
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; exit" INT
|
||||
trap "echo ''; echo '🛑 Stopping servers...'; kill $BACKEND_PID $FRONTEND_PID 2>/dev/null; echo '✅ Servers stopped'; exit" INT
|
||||
wait
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue