diff --git a/.gitignore b/.gitignore index 09566c2..3fdd994 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ backend/outputs/* !backend/uploads/.gitkeep !backend/outputs/.gitkeep +# Generated platform specifications +backend/platform_specs.json + # macOS .DS_Store diff --git a/ADMIN_GUIDE.md b/ADMIN_GUIDE.md new file mode 100644 index 0000000..0e80bc8 --- /dev/null +++ b/ADMIN_GUIDE.md @@ -0,0 +1,340 @@ +# Admin Panel Guide + +## Overview + +The Admin Panel allows you to manage platform specifications, add new platforms, edit existing configurations, and import/export settings. + +**Access:** http://localhost:8888/admin.html (or http://localhost:8000/admin.html for standard setup) + +--- + +## Features + +### πŸ“Š Metrics Dashboard + +View real-time statistics: +- **Total Platforms** - Number of configured platforms +- **Total Configurations** - Total format combinations across all platforms +- **Unique Codecs** - Number of different codecs in use +- **Aspect Ratios** - Total unique aspect ratios supported + +### πŸŽ› Platform Management + +**View All Platforms:** +- See all configured platforms with their specifications +- Each platform shows: Name, Key, Codec, Format count +- Detailed table of all aspect ratios with resolutions and bitrates + +**Add New Platform:** +1. Click **"+ Add New Platform"** +2. Fill in platform details: + - **Platform Key** (lowercase, no spaces - used in API) + - **Platform Name** (display name) + - **Video Codec** (H264, H265, or VP9) + - **Container Format** (MP4, WebM, MOV) +3. Add format configurations (aspect ratios): + - Aspect Ratio (e.g., 16:9) + - Resolution (e.g., 1920x1080) + - Bitrate (recommended, e.g., 1500k) + - Min/Max Bitrate (range) + - Audio Bitrate (e.g., 128k) + - Audio Codec (optional - defaults to AAC) + - Note (optional) +4. Click **"+ Add Format Configuration"** for multiple aspect ratios +5. Click **"Save Platform"** + +**Edit Platform:** +1. Click **"Edit"** button on any platform card +2. Modify settings (platform key cannot be changed) +3. Add/remove format configurations +4. Click **"Save Platform"** + +**Delete Platform:** +1. Click **"Delete"** button on any platform card +2. Confirm deletion +3. Platform is removed from system + +### πŸ“€ Export/Import + +**Export Specifications:** +- Click **"Export Specs (JSON)"** +- Downloads `platform_specs_YYYY-MM-DD.json` file +- Contains all platform configurations +- Use for backups or sharing + +**Import Specifications:** +- Click **"Import Specs (JSON)"** +- Select a previously exported JSON file +- Replaces ALL current specifications +- Use for restoring backups or bulk updates + +**Reload from Server:** +- Click **"Reload from Server"** +- Refreshes display with current backend data +- Use after manual changes or testing + +--- + +## Example: Adding a New Platform + +### Scenario: Add Instagram Reels + +**Platform Details:** +- **Key:** `instagram_reels` +- **Name:** `Instagram Reels` +- **Codec:** `libx264` (H264) +- **Container:** `mp4` + +**Format Configuration:** +- **Aspect Ratio:** `9:16` +- **Resolution:** `1080x1920` +- **Bitrate:** `2500k` +- **Min Bitrate:** `2000k` +- **Max Bitrate:** `3000k` +- **Audio Bitrate:** `128k` +- **Audio Codec:** `aac` +- **Note:** `Optimized for Instagram Reels` + +After saving, the platform will be immediately available in the main app! + +--- + +## Data Persistence + +### How Specs Are Saved + +When you add, edit, or delete platforms: + +1. Changes are applied to **in-memory** PLATFORM_SPECS +2. Automatically saved to **backend/platform_specs.json** +3. File is loaded on server startup +4. Changes persist across server restarts + +### File Location + +``` +backend/platform_specs.json +``` + +This file is auto-generated and excluded from Git (.gitignore). + +### Backup Strategy + +**Recommended:** +1. Regularly **export specs** via Admin Panel +2. Save exported JSON files with date stamps +3. Store backups in version control or cloud storage +4. Import when needed to restore + +--- + +## Platform Key Naming + +Platform keys are used in: +- API endpoints +- Filename detection +- Internal references + +**Rules:** +- Lowercase only +- No spaces (use underscores) +- Unique across all platforms +- Cannot be changed after creation + +**Good examples:** +- `tiktok` +- `meta` +- `youtube_ctv` +- `amazon_prime` + +**Bad examples:** +- `TikTok` (uppercase) +- `YouTube CTV` (spaces) +- `meta-fb` (hyphens - use underscores) + +--- + +## Format Configuration Details + +### Required Fields + +- **Aspect Ratio** - Format ratio (e.g., 16:9, 1:1, 9:16) +- **Resolution** - Width x Height (e.g., 1920x1080) +- **Bitrate** - Recommended video bitrate (e.g., 1500k) +- **Min Bitrate** - Minimum acceptable bitrate +- **Max Bitrate** - Maximum acceptable bitrate +- **Audio Bitrate** - Audio bitrate (e.g., 128k) + +### Optional Fields + +- **Audio Codec** - Override default audio codec (aac, opus, etc.) +- **Note** - Special instructions or warnings + +### Bitrate Format + +Use FFmpeg bitrate notation: +- `1500k` = 1500 kbps +- `15000k` = 15 Mbps +- `128k` = 128 kbps + +--- + +## Codec Reference + +### Video Codecs + +| Codec Value | Display Name | Best For | Notes | +|-------------|--------------|----------|-------| +| `libx264` | H264 | Universal compatibility | Most widely supported | +| `libx265` | H265/HEVC | Better compression | TikTok recommended | +| `libvpx-vp9` | VP9 | Quality & efficiency | YouTube preferred | + +### Container Formats + +| Container | Extensions | Compatible Codecs | +|-----------|-----------|-------------------| +| `mp4` | .mp4 | H264, H265 | +| `webm` | .webm | VP9 | +| `mov` | .mov | H264, H265 | + +### Audio Codecs + +- `aac` - Default, universal (for MP4) +- `opus` - High quality (for WebM/VP9) +- `mp3` - Legacy support + +--- + +## Validation + +The system validates: +- βœ… Duplicate platform keys (prevented) +- βœ… Required fields present +- βœ… Format array not empty +- βœ… Valid bitrate formats + +**Not automatically validated:** +- Resolution format (ensure it's `WIDTHxHEIGHT`) +- Aspect ratio accuracy +- Codec compatibility with container + +--- + +## API Endpoints + +Admin endpoints require backend access: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/admin/platforms` | POST | Add new platform | +| `/api/admin/platforms/` | PUT | Update platform | +| `/api/admin/platforms/` | 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 diff --git a/backend/app.py b/backend/app.py index 1e5ebe9..ffc7dcc 100644 --- a/backend/app.py +++ b/backend/app.py @@ -7,6 +7,7 @@ from flask_cors import CORS from werkzeug.utils import secure_filename import os import uuid +import json from datetime import datetime from video_processor import VideoProcessor from platform_specs import ( @@ -268,7 +269,165 @@ def cleanup_files(file_id): return jsonify({'error': str(e)}), 500 +# ============================================================================ +# ADMIN ENDPOINTS +# ============================================================================ + +SPECS_FILE = os.path.join(os.path.dirname(__file__), 'platform_specs.json') + + +def save_specs_to_file(specs): + """Save platform specifications to JSON file""" + try: + with open(SPECS_FILE, 'w') as f: + json.dump(specs, f, indent=2) + return True + except Exception as e: + print(f"Error saving specs: {e}") + return False + + +def load_specs_from_file(): + """Load platform specifications from JSON file""" + try: + if os.path.exists(SPECS_FILE): + with open(SPECS_FILE, 'r') as f: + return json.load(f) + return None + except Exception as e: + print(f"Error loading specs: {e}") + return None + + +@app.route('/api/admin/platforms', methods=['POST']) +def admin_add_platform(): + """Add a new platform configuration""" + try: + data = request.get_json() + + platform_key = data.get('key') + if not platform_key or platform_key in PLATFORM_SPECS: + return jsonify({'error': 'Invalid or duplicate platform key'}), 400 + + # Add to PLATFORM_SPECS + PLATFORM_SPECS[platform_key] = { + 'name': data.get('name'), + 'codec': data.get('codec'), + 'container': data.get('container', 'mp4'), + 'formats': data.get('formats', []) + } + + # Save to file + save_specs_to_file(PLATFORM_SPECS) + + return jsonify({ + 'success': True, + 'message': f'Platform {platform_key} added successfully' + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/admin/platforms/', methods=['PUT']) +def admin_update_platform(platform_key): + """Update an existing platform configuration""" + try: + data = request.get_json() + + if platform_key not in PLATFORM_SPECS: + return jsonify({'error': 'Platform not found'}), 404 + + # Update platform + PLATFORM_SPECS[platform_key] = { + 'name': data.get('name'), + 'codec': data.get('codec'), + 'container': data.get('container', 'mp4'), + 'formats': data.get('formats', []) + } + + # Save to file + save_specs_to_file(PLATFORM_SPECS) + + return jsonify({ + 'success': True, + 'message': f'Platform {platform_key} updated successfully' + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/admin/platforms/', methods=['DELETE']) +def admin_delete_platform(platform_key): + """Delete a platform configuration""" + try: + if platform_key not in PLATFORM_SPECS: + return jsonify({'error': 'Platform not found'}), 404 + + # Remove from PLATFORM_SPECS + del PLATFORM_SPECS[platform_key] + + # Save to file + save_specs_to_file(PLATFORM_SPECS) + + return jsonify({ + 'success': True, + 'message': f'Platform {platform_key} deleted successfully' + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/admin/export', methods=['GET']) +def admin_export_specs(): + """Export all platform specifications""" + try: + return jsonify({ + 'success': True, + 'specs': PLATFORM_SPECS, + 'exported_at': datetime.now().isoformat() + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/admin/import', methods=['POST']) +def admin_import_specs(): + """Import platform specifications from JSON""" + try: + data = request.get_json() + specs = data.get('specs') + + if not specs or not isinstance(specs, dict): + return jsonify({'error': 'Invalid specifications format'}), 400 + + # Replace all specs + PLATFORM_SPECS.clear() + PLATFORM_SPECS.update(specs) + + # Save to file + save_specs_to_file(PLATFORM_SPECS) + + return jsonify({ + 'success': True, + 'message': 'Specifications imported successfully', + 'platforms_count': len(PLATFORM_SPECS) + }) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + if __name__ == '__main__': + # Load specs from file if exists + saved_specs = load_specs_from_file() + if saved_specs: + PLATFORM_SPECS.clear() + PLATFORM_SPECS.update(saved_specs) + print(f"Loaded {len(saved_specs)} platform configurations from file") + # Check FFmpeg installation if not VideoProcessor.check_ffmpeg_installed(): print("WARNING: FFmpeg not found. Please install FFmpeg to use video conversion features.") @@ -277,4 +436,5 @@ if __name__ == '__main__': print("Starting Video Optimization Server...") 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) diff --git a/frontend/admin.css b/frontend/admin.css new file mode 100644 index 0000000..717ea39 --- /dev/null +++ b/frontend/admin.css @@ -0,0 +1,419 @@ +/* Admin Panel Additional Styles */ + +.header-actions { + margin-top: 1rem; +} + +.btn-link { + color: var(--primary-yellow); + text-decoration: none; + font-weight: 600; + padding: 0.5rem 1rem; + border: 2px solid var(--primary-yellow); + border-radius: 6px; + display: inline-block; + transition: all 0.3s ease; +} + +.btn-link:hover { + background-color: var(--primary-yellow); + color: var(--primary-black); +} + +/* Metrics Section */ +.metrics-section { + background-color: var(--secondary-black); + border-radius: 12px; + padding: 2rem; + margin-bottom: 2rem; +} + +.metrics-section h2 { + font-size: 1.75rem; + font-weight: 600; + color: var(--primary-yellow); + margin-bottom: 1.5rem; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; +} + +.metric-card { + background-color: var(--primary-black); + border: 2px solid var(--border-color); + border-radius: 8px; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; +} + +.metric-card:hover { + border-color: var(--primary-yellow); + transform: translateY(-2px); +} + +.metric-value { + font-size: 3rem; + font-weight: 700; + color: var(--primary-yellow); + margin-bottom: 0.5rem; +} + +.metric-label { + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; +} + +/* Actions Bar */ +.actions-bar { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.actions-bar .btn-primary, +.actions-bar .btn-secondary { + flex: 1; + min-width: 200px; +} + +/* Platforms Section */ +.platforms-section { + margin-bottom: 3rem; +} + +.platforms-section h2 { + font-size: 1.75rem; + font-weight: 600; + color: var(--primary-yellow); + margin-bottom: 1.5rem; +} + +.platform-card { + background-color: var(--secondary-black); + border: 2px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + transition: all 0.3s ease; +} + +.platform-card:hover { + border-color: var(--primary-yellow); +} + +.platform-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.platform-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-yellow); +} + +.platform-key { + font-size: 0.875rem; + color: var(--text-muted); + font-family: 'Courier New', monospace; +} + +.platform-actions { + display: flex; + gap: 0.5rem; +} + +.btn-icon { + padding: 0.5rem 1rem; + background-color: transparent; + color: var(--primary-yellow); + border: 1px solid var(--primary-yellow); + border-radius: 6px; + font-family: 'Montserrat', sans-serif; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-icon:hover { + background-color: var(--primary-yellow); + color: var(--primary-black); +} + +.btn-icon.danger { + border-color: #ff0000; + color: #ff0000; +} + +.btn-icon.danger:hover { + background-color: #ff0000; + color: white; +} + +.platform-info { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.info-badge { + background-color: var(--primary-black); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 0.75rem; +} + +.info-badge-label { + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.info-badge-value { + font-size: 1rem; + color: var(--text-primary); + font-weight: 600; +} + +.formats-table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +.formats-table th, +.formats-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.formats-table th { + background-color: var(--primary-black); + color: var(--primary-yellow); + font-weight: 600; + font-size: 0.875rem; +} + +.formats-table td { + color: var(--text-secondary); + font-size: 0.875rem; +} + +.formats-table tr:hover { + background-color: rgba(255, 196, 7, 0.05); +} + +/* Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + z-index: 1000; + overflow-y: auto; + padding: 2rem; +} + +.modal-content { + background-color: var(--secondary-black); + border: 2px solid var(--primary-yellow); + border-radius: 12px; + max-width: 900px; + margin: 0 auto; + padding: 0; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 2px solid var(--border-color); +} + +.modal-header h2 { + color: var(--primary-yellow); + font-size: 1.5rem; + margin: 0; +} + +.modal-close { + background: none; + border: none; + color: var(--text-primary); + font-size: 2rem; + cursor: pointer; + padding: 0; + width: 40px; + height: 40px; + line-height: 40px; + text-align: center; + transition: all 0.3s ease; +} + +.modal-close:hover { + color: var(--primary-yellow); + transform: rotate(90deg); +} + +.modal-body { + padding: 2rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.form-group h3 { + color: var(--primary-yellow); + font-size: 1.25rem; + margin: 2rem 0 1rem 0; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.format-config { + background-color: var(--primary-black); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + position: relative; +} + +.format-config-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.format-config-title { + color: var(--primary-yellow); + font-weight: 600; +} + +.btn-remove { + background: none; + border: none; + color: #ff0000; + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + line-height: 30px; + text-align: center; +} + +.btn-remove:hover { + color: #ff6666; +} + +.format-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.format-fields .form-group { + margin-bottom: 0; +} + +.modal-actions { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-color); +} + +.modal-actions .btn-primary, +.modal-actions .btn-secondary { + flex: 1; +} + +/* Success/Error Messages */ +.message { + padding: 1rem; + border-radius: 6px; + margin-bottom: 1rem; + font-weight: 600; +} + +.message.success { + background-color: rgba(0, 255, 0, 0.1); + border: 2px solid #00ff00; + color: #00ff00; +} + +.message.error { + background-color: rgba(255, 0, 0, 0.1); + border: 2px solid #ff0000; + color: #ff6666; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 3rem; + color: var(--text-muted); +} + +.empty-state h3 { + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .modal { + padding: 1rem; + } + + .platform-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .platform-actions { + width: 100%; + justify-content: flex-end; + } + + .actions-bar { + flex-direction: column; + } + + .actions-bar .btn-primary, + .actions-bar .btn-secondary { + width: 100%; + } + + .format-fields { + grid-template-columns: 1fr; + } +} diff --git a/frontend/admin.html b/frontend/admin.html new file mode 100644 index 0000000..b313a19 --- /dev/null +++ b/frontend/admin.html @@ -0,0 +1,129 @@ + + + + + + Admin Panel - Video Optimizer + + + + + + + +
+ +
+

Admin Panel

+

Platform Specifications Management

+ +
+ + +
+

Current Metrics

+
+
+
0
+
Total Platforms
+
+
+
0
+
Total Configurations
+
+
+
0
+
Unique Codecs
+
+
+
0
+
Aspect Ratios
+
+
+
+ + +
+ + + + + +
+ + +
+

Platform Specifications

+
+ +
+
+ + + + + +
+

L'OrΓ©al Video Optimizer - Admin Panel

+
+
+ + + + + diff --git a/frontend/admin.js b/frontend/admin.js new file mode 100644 index 0000000..47a4e77 --- /dev/null +++ b/frontend/admin.js @@ -0,0 +1,407 @@ +// Admin Panel JavaScript +// API Configuration +const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api'; + +// State +let platforms = []; +let editingPlatformKey = null; +let formatCounter = 0; + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadPlatforms(); + setupEventListeners(); +}); + +// Event Listeners +function setupEventListeners() { + document.getElementById('addPlatformBtn').addEventListener('click', () => openModal()); + document.getElementById('exportBtn').addEventListener('click', exportSpecs); + document.getElementById('importBtn').addEventListener('click', () => document.getElementById('importFile').click()); + document.getElementById('importFile').addEventListener('change', importSpecs); + document.getElementById('reloadBtn').addEventListener('click', loadPlatforms); + document.getElementById('closeModal').addEventListener('click', closeModal); + document.getElementById('cancelBtn').addEventListener('click', closeModal); + document.getElementById('addFormatBtn').addEventListener('click', addFormatConfig); + document.getElementById('platformForm').addEventListener('submit', savePlatform); +} + +// Load Platforms +async function loadPlatforms() { + try { + const response = await fetch(`${API_BASE}/platforms`); + const data = await response.json(); + platforms = data.platforms; + + updateMetrics(); + renderPlatforms(); + } catch (error) { + console.error('Error loading platforms:', error); + showMessage('Error loading platforms: ' + error.message, 'error'); + } +} + +// Update Metrics +function updateMetrics() { + const totalPlatforms = platforms.length; + const totalFormats = platforms.reduce((sum, p) => sum + p.formats.length, 0); + const codecs = new Set(platforms.map(p => p.codec)); + const aspectRatios = new Set(); + + platforms.forEach(p => { + p.formats.forEach(f => aspectRatios.add(f.ratio)); + }); + + document.getElementById('totalPlatforms').textContent = totalPlatforms; + document.getElementById('totalFormats').textContent = totalFormats; + document.getElementById('totalCodecs').textContent = codecs.size; + document.getElementById('totalAspectRatios').textContent = aspectRatios.size; +} + +// Render Platforms +function renderPlatforms() { + const container = document.getElementById('platformsList'); + + if (platforms.length === 0) { + container.innerHTML = ` +
+

No platforms configured

+

Click "Add New Platform" to get started

+
+ `; + return; + } + + container.innerHTML = platforms.map(platform => ` +
+
+
+
${platform.name}
+
Key: ${platform.key}
+
+
+ + +
+
+
+
+
Codec
+
${platform.codec}
+
+
+
Formats
+
${platform.formats.length} configurations
+
+
+ + + + + + + + + + + + ${platform.formats.map(format => ` + + + + + + + + `).join('')} + +
Aspect RatioResolutionBitrateBitrate RangeAudio
${format.ratio}${format.size}${format.bitrate}${format.bitrate_min} - ${format.bitrate_max}${format.audio}
+
+ `).join(''); +} + +// Modal Functions +function openModal(platform = null) { + editingPlatformKey = platform ? platform.key : null; + const modal = document.getElementById('platformModal'); + const title = document.getElementById('modalTitle'); + + if (platform) { + title.textContent = `Edit Platform: ${platform.name}`; + populateForm(platform); + } else { + title.textContent = 'Add New Platform'; + resetForm(); + } + + modal.style.display = 'block'; +} + +function closeModal() { + document.getElementById('platformModal').style.display = 'none'; + resetForm(); +} + +function resetForm() { + document.getElementById('platformForm').reset(); + document.getElementById('formatsContainer').innerHTML = ''; + editingPlatformKey = null; + formatCounter = 0; + + // Add one default format config + addFormatConfig(); +} + +function populateForm(platform) { + document.getElementById('platformKey').value = platform.key; + document.getElementById('platformKey').disabled = true; // Don't allow key changes + document.getElementById('platformName').value = platform.name; + document.getElementById('platformCodec').value = platform.codec; + document.getElementById('platformContainer').value = platform.container || 'mp4'; + + // Clear and populate formats + document.getElementById('formatsContainer').innerHTML = ''; + platform.formats.forEach(format => addFormatConfig(format)); +} + +// Format Configuration Management +function addFormatConfig(formatData = null) { + const container = document.getElementById('formatsContainer'); + const formatId = `format-${formatCounter++}`; + + const formatDiv = document.createElement('div'); + formatDiv.className = 'format-config'; + formatDiv.id = formatId; + formatDiv.innerHTML = ` +
+
Format Configuration ${formatCounter}
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + container.appendChild(formatDiv); +} + +function removeFormat(formatId) { + const formatDiv = document.getElementById(formatId); + if (formatDiv) { + formatDiv.remove(); + } +} + +// Save Platform +async function savePlatform(e) { + e.preventDefault(); + + const platformKey = document.getElementById('platformKey').value.trim().toLowerCase(); + const platformName = document.getElementById('platformName').value.trim(); + const codec = document.getElementById('platformCodec').value; + const container = document.getElementById('platformContainer').value; + + // Collect all format configurations + const formatConfigs = []; + const formatDivs = document.querySelectorAll('.format-config'); + + formatDivs.forEach(div => { + const format = { + ratio: div.querySelector('.ratio-input').value.trim(), + size: div.querySelector('.size-input').value.trim(), + bitrate: div.querySelector('.bitrate-input').value.trim(), + bitrate_min: div.querySelector('.bitrate-min-input').value.trim(), + bitrate_max: div.querySelector('.bitrate-max-input').value.trim(), + audio: div.querySelector('.audio-input').value.trim() + }; + + const audioCodec = div.querySelector('.audio-codec-input').value.trim(); + const note = div.querySelector('.note-input').value.trim(); + + if (audioCodec) format.audio_codec = audioCodec; + if (note) format.note = note; + + formatConfigs.push(format); + }); + + if (formatConfigs.length === 0) { + showMessage('Please add at least one format configuration', 'error'); + return; + } + + const platformData = { + key: platformKey, + name: platformName, + codec: codec, + container: container, + formats: formatConfigs + }; + + try { + const url = editingPlatformKey + ? `${API_BASE}/admin/platforms/${editingPlatformKey}` + : `${API_BASE}/admin/platforms`; + + const method = editingPlatformKey ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(platformData) + }); + + const result = await response.json(); + + if (result.success) { + showMessage(`Platform ${editingPlatformKey ? 'updated' : 'added'} successfully!`, 'success'); + closeModal(); + loadPlatforms(); + } else { + showMessage('Error: ' + result.error, 'error'); + } + } catch (error) { + showMessage('Error saving platform: ' + error.message, 'error'); + } +} + +// Edit Platform +function editPlatform(platformKey) { + const platform = platforms.find(p => p.key === platformKey); + if (platform) { + openModal(platform); + } +} + +// Delete Platform +async function deletePlatform(platformKey) { + if (!confirm(`Are you sure you want to delete platform "${platformKey}"?`)) { + return; + } + + try { + const response = await fetch(`${API_BASE}/admin/platforms/${platformKey}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + showMessage('Platform deleted successfully!', 'success'); + loadPlatforms(); + } else { + showMessage('Error: ' + result.error, 'error'); + } + } catch (error) { + showMessage('Error deleting platform: ' + error.message, 'error'); + } +} + +// Export/Import Functions +async function exportSpecs() { + try { + const response = await fetch(`${API_BASE}/admin/export`); + const data = await response.json(); + + if (data.success) { + const blob = new Blob([JSON.stringify(data.specs, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `platform_specs_${new Date().toISOString().split('T')[0]}.json`; + a.click(); + URL.revokeObjectURL(url); + + showMessage('Specifications exported successfully!', 'success'); + } else { + showMessage('Error exporting: ' + data.error, 'error'); + } + } catch (error) { + showMessage('Error exporting specs: ' + error.message, 'error'); + } +} + +async function importSpecs(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const specs = JSON.parse(event.target.result); + + const response = await fetch(`${API_BASE}/admin/import`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ specs: specs }) + }); + + const result = await response.json(); + + if (result.success) { + showMessage('Specifications imported successfully!', 'success'); + loadPlatforms(); + } else { + showMessage('Error importing: ' + result.error, 'error'); + } + } catch (error) { + showMessage('Error importing specs: ' + error.message, 'error'); + } + }; + + reader.readAsText(file); + e.target.value = ''; // Reset file input +} + +// UI Helpers +function showMessage(message, type = 'success') { + const existingMessage = document.querySelector('.message'); + if (existingMessage) { + existingMessage.remove(); + } + + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${type}`; + messageDiv.textContent = message; + + const container = document.querySelector('.container'); + container.insertBefore(messageDiv, container.firstChild.nextSibling); + + setTimeout(() => { + messageDiv.remove(); + }, 5000); +} diff --git a/frontend/index.html b/frontend/index.html index 6d849e1..4134860 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -142,6 +142,7 @@