ChandimaPrabath commited on
Commit
e2c1f2e
·
1 Parent(s): 2db4881
DOCS.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Distributed Media Encoder Service
2
+
3
+ A distributed video encoding service with a modern dark-themed UI, supporting multiple encoding instances for high-performance video processing.
4
+
5
+ ## Features
6
+
7
+ - Modern dark-themed web interface with drag-and-drop
8
+ - Distributed video encoding across multiple workers
9
+ - Multiple quality outputs (480p, 720p, 1080p)
10
+ - Real-time encoding progress tracking
11
+ - Single-port access through Python proxy
12
+ - Temporary directory support for restricted environments
13
+
14
+ ## System Architecture
15
+
16
+ - **Web Interface**: Modern dark theme UI for video upload and monitoring
17
+ - **Task Queue**: Redis-based distributed task queue
18
+ - **Workers**: Multiple Celery workers for distributed encoding
19
+ - **Storage**: Temporary directory based file storage
20
+ - **Proxy**: Python-based reverse proxy for single-port access
21
+
22
+ ## Prerequisites
23
+
24
+ - Python 3.8+
25
+ - FFmpeg
26
+ - Redis
27
+
28
+ ## Quick Start
29
+
30
+ 1. Install dependencies:
31
+ ```bash
32
+ pip install -r requirements.txt
33
+ ```
34
+
35
+ 2. Start the service:
36
+ ```bash
37
+ python3 start_service.py --port 5000 --tmp-dir /tmp/encoder
38
+ ```
39
+
40
+ The service will be available at `http://localhost:5000`
41
+
42
+ ## Distributed Setup
43
+
44
+ ### Main Instance
45
+ ```bash
46
+ python3 start_service.py --port 5000 --tmp-dir /tmp/encoder
47
+ ```
48
+
49
+ ### Additional Worker Instances
50
+ ```bash
51
+ # Worker 1
52
+ python3 start_service.py --worker-name encoder_worker_1 --tmp-dir /tmp/encoder_1
53
+
54
+ # Worker 2
55
+ python3 start_service.py --worker-name encoder_worker_2 --tmp-dir /tmp/encoder_2
56
+
57
+ # Add more workers as needed
58
+ ```
59
+
60
+ ## Command Line Options
61
+
62
+ - `--port`: Port number for the web interface (default: 5000)
63
+ - `--tmp-dir`: Temporary directory for storage (default: /tmp/encoder)
64
+ - `--worker-name`: Custom name for worker instance
65
+
66
+ ## Directory Structure
67
+
68
+ ```
69
+ /tmp/encoder/
70
+ ├── uploads/ # Uploaded video files
71
+ ├── encoded/ # Encoded video files
72
+ ├── redis.conf # Redis configuration
73
+ ├── redis.rdb # Redis database
74
+ └── redis.log # Redis logs
75
+ ```
76
+
77
+ ## Monitoring
78
+
79
+ ### View Service Logs
80
+ ```bash
81
+ tail -f /tmp/encoder/redis.log # Redis logs
82
+ ```
83
+
84
+ ### Check Worker Status
85
+ ```python
86
+ from app.services.encoder_service import celery
87
+ celery.control.inspect().active() # View active tasks
88
+ celery.control.inspect().registered() # View registered workers
89
+ ```
90
+
91
+ ## Scaling
92
+
93
+ The service can be scaled by adding more worker instances. Each worker should:
94
+ 1. Have FFmpeg installed
95
+ 2. Connect to the same Redis instance
96
+ 3. Have access to the shared storage directory
97
+
98
+ ### Example Scaling Setup
99
+
100
+ ```bash
101
+ # Main instance (handles web UI and coordination)
102
+ python3 start_service.py --port 5000 --tmp-dir /shared/tmp/encoder
103
+
104
+ # Worker instances (handle encoding tasks)
105
+ python3 start_service.py --worker-name encoder_worker_1 --tmp-dir /shared/tmp/encoder
106
+ python3 start_service.py --worker-name encoder_worker_2 --tmp-dir /shared/tmp/encoder
107
+ ```
108
+
109
+ ## Performance Considerations
110
+
111
+ 1. **Storage Performance**:
112
+ - Use fast storage for /tmp directory
113
+ - Consider local SSD for better I/O performance
114
+
115
+ 2. **Network Bandwidth**:
116
+ - Ensure sufficient bandwidth between workers
117
+ - Consider network topology when distributing workers
118
+
119
+ 3. **Resource Allocation**:
120
+ - Monitor CPU usage across workers
121
+ - Balance memory usage for concurrent encoding
122
+
123
+ ## Troubleshooting
124
+
125
+ 1. **Service Won't Start**:
126
+ ```bash
127
+ # Check if Redis is already running
128
+ ps aux | grep redis
129
+ # Check port availability
130
+ netstat -tulpn | grep 5000
131
+ ```
132
+
133
+ 2. **Encoding Fails**:
134
+ - Verify FFmpeg installation
135
+ - Check temporary directory permissions
136
+ - Verify Redis connectivity
137
+
138
+ 3. **Worker Connection Issues**:
139
+ - Check Redis connection string
140
+ - Verify network connectivity
141
+ - Check firewall settings
142
+
143
+ ## Security Notes
144
+
145
+ 1. **Temporary Directory**:
146
+ - Regularly clean up old files
147
+ - Set appropriate permissions
148
+ ```bash
149
+ chmod 700 /tmp/encoder
150
+ ```
151
+
152
+ 2. **Redis Security**:
153
+ - Use strong passwords in production
154
+ - Configure proper network restrictions
155
+
156
+ 3. **File Validation**:
157
+ - Service validates video file types
158
+ - Implements size restrictions
159
+ - Sanitizes filenames
160
+
161
+ ## API Endpoints
162
+
163
+ - `POST /api/upload`: Upload video file
164
+ - `GET /api/status/<job_id>`: Get encoding status
165
+ - `GET /api/video/<job_id>/<quality>`: Stream encoded video
166
+ - `GET /api/jobs`: List all encoding jobs
167
+
168
+ ## Contributing
169
+
170
+ [Contribution Guidelines]
171
+
172
+ ## License
173
+
174
+ [Your License Here]
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.9 as the base image
2
+ FROM python:3.9
3
+
4
+ # Install FFmpeg
5
+ RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Create a non-root user
8
+ RUN useradd -m -u 1000 user
9
+ USER user
10
+
11
+ # Set up environment variables
12
+ ENV PATH="/home/user/.local/bin:$PATH"
13
+
14
+ # Set the working directory
15
+ WORKDIR /app
16
+
17
+ # Copy and install dependencies
18
+ COPY --chown=user ./requirements.txt requirements.txt
19
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
20
+
21
+ # Copy application files
22
+ COPY --chown=user . /app
23
+
24
+ # Expose the required port
25
+ EXPOSE 7860
26
+
27
+ # Run the service
28
+ CMD ["python3", "start_service.py", "--port", "7860", "--tmp-dir", "/tmp/encoder_main"]
app/__init__.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template
2
+ from app.routes import main_bp, api_bp
3
+ from app.proxy import proxy_bp
4
+ import os
5
+
6
+ def create_app():
7
+ app = Flask(__name__,
8
+ static_folder='static',
9
+ template_folder='templates')
10
+
11
+ # Load configuration
12
+ app.config.from_object('app.config')
13
+
14
+ # Ensure upload and encoded directories exist
15
+ upload_dir = os.getenv('UPLOAD_FOLDER', 'uploads')
16
+ encoded_dir = os.getenv('ENCODED_FOLDER', 'encoded')
17
+ os.makedirs(upload_dir, exist_ok=True)
18
+ os.makedirs(encoded_dir, exist_ok=True)
19
+
20
+ # Set upload folder in app config
21
+ app.config['UPLOAD_FOLDER'] = upload_dir
22
+ app.config['ENCODED_FOLDER'] = encoded_dir
23
+ app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 # 1GB max file size
24
+
25
+ # Register blueprints
26
+ app.register_blueprint(main_bp)
27
+ app.register_blueprint(api_bp, url_prefix='/api')
28
+ app.register_blueprint(proxy_bp)
29
+
30
+ @app.route('/')
31
+ def index():
32
+ return render_template('index.html')
33
+
34
+ @app.route('/favicon.ico')
35
+ def favicon():
36
+ return app.send_static_file('favicon.ico')
37
+
38
+ return app
app/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (1.23 kB). View file
 
app/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (1.93 kB). View file
 
app/__pycache__/config.cpython-310.pyc ADDED
Binary file (836 Bytes). View file
 
app/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.22 kB). View file
 
app/__pycache__/error_handlers.cpython-310.pyc ADDED
Binary file (1.58 kB). View file
 
app/__pycache__/proxy.cpython-310.pyc ADDED
Binary file (1.9 kB). View file
 
app/__pycache__/proxy.cpython-313.pyc ADDED
Binary file (3.15 kB). View file
 
app/__pycache__/routes.cpython-310.pyc ADDED
Binary file (6 kB). View file
 
app/__pycache__/routes.cpython-313.pyc ADDED
Binary file (10.1 kB). View file
 
app/__pycache__/utils.cpython-310.pyc ADDED
Binary file (2.52 kB). View file
 
app/config.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
+ # Base directory
5
+ BASE_DIR = Path(__file__).resolve().parent.parent
6
+
7
+ # Flask configuration
8
+ DEBUG = True
9
+ SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
10
+
11
+ # File upload configuration
12
+ UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER', BASE_DIR / 'uploads')
13
+ ENCODED_FOLDER = os.getenv('ENCODED_FOLDER', BASE_DIR / 'encoded')
14
+ MAX_CONTENT_LENGTH = 1024 * 1024 * 1024 # 1GB max file size
15
+ ALLOWED_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'wmv'}
16
+
17
+ # Celery configuration
18
+ CELERY_BROKER_URL = 'redis://localhost:6379/0'
19
+ CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
20
+ CELERY_TASK_SERIALIZER = 'json'
21
+ CELERY_RESULT_SERIALIZER = 'json'
22
+ CELERY_ACCEPT_CONTENT = ['json']
23
+ CELERY_TASK_TRACK_STARTED = True
24
+ CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
25
+
26
+ # Redis configuration
27
+ REDIS_URL = 'redis://localhost:6379/0'
28
+
29
+ # Create required directories
30
+ Path(UPLOAD_FOLDER).mkdir(parents=True, exist_ok=True)
31
+ Path(ENCODED_FOLDER).mkdir(parents=True, exist_ok=True)
app/error_handlers.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import jsonify
2
+ from werkzeug.exceptions import HTTPException
3
+ import logging
4
+
5
+ def register_error_handlers(app):
6
+ # Set up logging
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @app.errorhandler(HTTPException)
11
+ def handle_http_error(error):
12
+ """Handle all HTTP exceptions."""
13
+ response = {
14
+ 'error': True,
15
+ 'message': error.description,
16
+ 'status_code': error.code
17
+ }
18
+ logger.error(f'HTTP error occurred: {error.code} - {error.description}')
19
+ return jsonify(response), error.code
20
+
21
+ @app.errorhandler(Exception)
22
+ def handle_generic_error(error):
23
+ """Handle all unhandled exceptions."""
24
+ response = {
25
+ 'error': True,
26
+ 'message': 'An unexpected error occurred',
27
+ 'status_code': 500
28
+ }
29
+ logger.error(f'Unhandled error: {str(error)}', exc_info=True)
30
+ return jsonify(response), 500
31
+
32
+ @app.errorhandler(413)
33
+ def handle_file_too_large(error):
34
+ """Handle file size exceeding MAX_CONTENT_LENGTH."""
35
+ response = {
36
+ 'error': True,
37
+ 'message': 'File is too large. Maximum size allowed is 1GB.',
38
+ 'status_code': 413
39
+ }
40
+ logger.error('File upload exceeded size limit')
41
+ return jsonify(response), 413
app/proxy.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, send_from_directory, current_app
2
+ import os
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+ proxy_bp = Blueprint('proxy', __name__)
7
+
8
+ @proxy_bp.route('/files/<path:filename>')
9
+ def serve_file(filename):
10
+ """Serve files from the encoded directory"""
11
+ try:
12
+ encoded_dir = current_app.config['ENCODED_FOLDER']
13
+ # Split the filename to get the job_id and quality
14
+ parts = filename.split('/')
15
+ if len(parts) >= 2:
16
+ job_id = parts[0]
17
+ quality_file = parts[1]
18
+ # Construct the full path
19
+ job_dir = os.path.join(encoded_dir, job_id)
20
+ return send_from_directory(job_dir, quality_file)
21
+ else:
22
+ return {"error": "Invalid file path"}, 400
23
+ except Exception as e:
24
+ logger.error(f"Error serving file {filename}: {str(e)}")
25
+ return {"error": "File not found"}, 404
26
+
27
+ @proxy_bp.route('/uploads/<path:filename>')
28
+ def serve_upload(filename):
29
+ """Serve files from the uploads directory"""
30
+ try:
31
+ upload_dir = current_app.config['UPLOAD_FOLDER']
32
+ return send_from_directory(upload_dir, filename)
33
+ except Exception as e:
34
+ logger.error(f"Error serving upload {filename}: {str(e)}")
35
+ return {"error": "File not found"}, 404
36
+
37
+ @proxy_bp.route('/video/<job_id>/<quality>')
38
+ def serve_video(job_id, quality):
39
+ """Serve encoded video files"""
40
+ try:
41
+ encoded_dir = current_app.config['ENCODED_FOLDER']
42
+ video_path = os.path.join(encoded_dir, job_id, f"{quality}.mp4")
43
+ if os.path.exists(video_path):
44
+ return send_from_directory(
45
+ os.path.join(encoded_dir, job_id),
46
+ f"{quality}.mp4",
47
+ mimetype='video/mp4'
48
+ )
49
+ else:
50
+ return {"error": "Video not found"}, 404
51
+ except Exception as e:
52
+ logger.error(f"Error serving video {job_id}/{quality}: {str(e)}")
53
+ return {"error": "Video not found"}, 404
app/routes.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, current_app, render_template, send_from_directory
2
+ from werkzeug.utils import secure_filename
3
+ import os
4
+ from pathlib import Path
5
+ import logging
6
+ import json
7
+ from datetime import datetime
8
+
9
+ from app.services.encoder_service import encoder_service
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Create blueprints
14
+ main_bp = Blueprint('main', __name__)
15
+ api_bp = Blueprint('api', __name__)
16
+
17
+ @main_bp.route('/')
18
+ def index():
19
+ """Render the main page"""
20
+ return render_template('index.html')
21
+
22
+ @main_bp.route('/files')
23
+ def files_page():
24
+ """Render the files page"""
25
+ return render_template('files.html')
26
+
27
+ @api_bp.route('/files')
28
+ def list_files():
29
+ """List all encoded files with their details"""
30
+ try:
31
+ encoded_dir = Path(current_app.config['ENCODED_FOLDER'])
32
+ files = []
33
+
34
+ if encoded_dir.exists():
35
+ for job_dir in encoded_dir.iterdir():
36
+ if job_dir.is_dir():
37
+ job_id = job_dir.name
38
+ job_info = encoder_service.get_job_info(job_id)
39
+
40
+ if job_info and job_info.get('status') == 'completed':
41
+ files.append({
42
+ 'job_id': job_id,
43
+ 'output_name': job_info.get('output_name', ''),
44
+ 'created_at': job_info.get('start_time'),
45
+ 'completed_at': job_info.get('completion_time'),
46
+ 'qualities': {
47
+ file['quality']: file['size']
48
+ for file in job_info.get('files', [])
49
+ }
50
+ })
51
+
52
+ return jsonify({
53
+ 'files': sorted(files, key=lambda x: x['created_at'], reverse=True)
54
+ })
55
+ except Exception as e:
56
+ logger.error(f"Failed to list files: {str(e)}")
57
+ return jsonify({
58
+ 'error': True,
59
+ 'message': 'Failed to list files'
60
+ }), 500
61
+
62
+ @api_bp.route('/upload', methods=['POST'])
63
+ def upload_video():
64
+ """Handle video file upload and start encoding process"""
65
+ try:
66
+ if 'video' not in request.files:
67
+ return jsonify({
68
+ 'error': True,
69
+ 'message': 'No video file provided'
70
+ }), 400
71
+
72
+ file = request.files['video']
73
+ output_name = request.form.get('output_name')
74
+
75
+ if file.filename == '':
76
+ return jsonify({
77
+ 'error': True,
78
+ 'message': 'No file selected'
79
+ }), 400
80
+
81
+ if not allowed_file(file.filename):
82
+ return jsonify({
83
+ 'error': True,
84
+ 'message': 'Invalid file type'
85
+ }), 400
86
+
87
+ filename = secure_filename(file.filename)
88
+ file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
89
+
90
+ # Save the file
91
+ file.save(file_path)
92
+
93
+ # Get custom encoding settings if provided
94
+ settings = request.form.get('settings')
95
+
96
+ # Generate job ID and start encoding
97
+ job_id = generate_job_id()
98
+ result = encoder_service.start_encode_job(
99
+ filename=filename,
100
+ job_id=job_id,
101
+ output_name=output_name,
102
+ settings=settings
103
+ )
104
+
105
+ return jsonify({
106
+ 'job_id': job_id,
107
+ 'message': 'Video upload successful',
108
+ 'status': result['status']
109
+ }), 202
110
+
111
+ except Exception as e:
112
+ logger.error(f"Upload failed: {str(e)}")
113
+ return jsonify({
114
+ 'error': True,
115
+ 'message': 'Failed to process upload'
116
+ }), 500
117
+
118
+ @api_bp.route('/jobs', methods=['GET'])
119
+ def list_jobs():
120
+ """List all encoding jobs"""
121
+ try:
122
+ return jsonify({
123
+ 'jobs': encoder_service.jobs
124
+ }), 200
125
+ except Exception as e:
126
+ logger.error(f"Failed to list jobs: {str(e)}")
127
+ return jsonify({
128
+ 'error': True,
129
+ 'message': 'Failed to list jobs'
130
+ }), 500
131
+
132
+ @api_bp.route('/jobs/<job_id>/stop', methods=['POST'])
133
+ def stop_job(job_id):
134
+ """Stop an encoding job"""
135
+ try:
136
+ if encoder_service.stop_job(job_id):
137
+ return jsonify({
138
+ 'message': 'Job stopped successfully'
139
+ }), 200
140
+ else:
141
+ return jsonify({
142
+ 'error': True,
143
+ 'message': 'Job not found or already completed'
144
+ }), 404
145
+ except Exception as e:
146
+ logger.error(f"Failed to stop job: {str(e)}")
147
+ return jsonify({
148
+ 'error': True,
149
+ 'message': 'Failed to stop job'
150
+ }), 500
151
+
152
+ @api_bp.route('/jobs/<job_id>/clean', methods=['POST'])
153
+ def clean_job(job_id):
154
+ """Clean up all files related to a job"""
155
+ try:
156
+ if encoder_service.clean_job(job_id):
157
+ return jsonify({
158
+ 'message': 'Job cleaned successfully'
159
+ }), 200
160
+ else:
161
+ return jsonify({
162
+ 'error': True,
163
+ 'message': 'Failed to clean job'
164
+ }), 500
165
+ except Exception as e:
166
+ logger.error(f"Failed to clean job: {str(e)}")
167
+ return jsonify({
168
+ 'error': True,
169
+ 'message': 'Failed to clean job'
170
+ }), 500
171
+
172
+ @api_bp.route('/status/<job_id>', methods=['GET'])
173
+ def get_job_status(job_id):
174
+ """Get the status of an encoding job"""
175
+ try:
176
+ status = encoder_service.get_job_status(job_id)
177
+ return jsonify(status), 200
178
+ except Exception as e:
179
+ logger.error(f"Failed to get status for job {job_id}: {str(e)}")
180
+ return jsonify({
181
+ 'error': True,
182
+ 'message': 'Failed to get job status'
183
+ }), 500
184
+
185
+ @api_bp.route('/video/<job_id>/<quality>')
186
+ def serve_video(job_id, quality):
187
+ """Serve encoded video files"""
188
+ try:
189
+ job_info = encoder_service.get_job_info(job_id)
190
+ if not job_info:
191
+ return jsonify({
192
+ 'error': True,
193
+ 'message': 'Job not found'
194
+ }), 404
195
+
196
+ encoded_dir = current_app.config['ENCODED_FOLDER']
197
+ output_name = job_info.get('output_name', 'video')
198
+ video_filename = f"{output_name}_{quality}.mp4"
199
+ video_path = os.path.join(encoded_dir, job_id, video_filename)
200
+
201
+ if os.path.exists(video_path):
202
+ return send_from_directory(
203
+ os.path.join(encoded_dir, job_id),
204
+ video_filename,
205
+ mimetype='video/mp4',
206
+ as_attachment=True,
207
+ download_name=video_filename
208
+ )
209
+ else:
210
+ return jsonify({
211
+ 'error': True,
212
+ 'message': 'Video not found'
213
+ }), 404
214
+ except Exception as e:
215
+ logger.error(f"Failed to serve video: {str(e)}")
216
+ return jsonify({
217
+ 'error': True,
218
+ 'message': 'Failed to serve video'
219
+ }), 500
220
+
221
+ def allowed_file(filename):
222
+ """Check if the file extension is allowed"""
223
+ ALLOWED_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'wmv'}
224
+ return '.' in filename and \
225
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
226
+
227
+ def generate_job_id():
228
+ """Generate a unique job ID"""
229
+ import uuid
230
+ return str(uuid.uuid4())
app/services/__pycache__/encoder_service.cpython-310.pyc ADDED
Binary file (7.33 kB). View file
 
app/services/__pycache__/encoder_service.cpython-313.pyc ADDED
Binary file (12.9 kB). View file
 
app/services/encoder_service.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+ import logging
6
+ import json
7
+ from datetime import datetime
8
+ import re
9
+ import threading
10
+ import queue
11
+ import signal
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class EncoderService:
18
+ def __init__(self):
19
+ self.jobs = {}
20
+ # Optimized settings for web streaming
21
+ self.default_qualities = {
22
+ '480p': {
23
+ 'width': 854,
24
+ 'height': 480,
25
+ 'bitrate': '1000k',
26
+ 'maxrate': '1500k',
27
+ 'bufsize': '2000k',
28
+ 'audio_bitrate': '128k',
29
+ 'keyframe': '48', # Keyframe every 2 seconds at 24fps
30
+ 'preset': 'slow', # Better compression
31
+ 'profile': 'main',
32
+ 'level': '3.1',
33
+ 'tune': 'fastdecode'
34
+ },
35
+ '720p': {
36
+ 'width': 1280,
37
+ 'height': 720,
38
+ 'bitrate': '2500k',
39
+ 'maxrate': '3000k',
40
+ 'bufsize': '4000k',
41
+ 'audio_bitrate': '128k',
42
+ 'keyframe': '48',
43
+ 'preset': 'slow',
44
+ 'profile': 'main',
45
+ 'level': '3.1',
46
+ 'tune': 'fastdecode'
47
+ },
48
+ '1080p': {
49
+ 'width': 1920,
50
+ 'height': 1080,
51
+ 'bitrate': '5000k',
52
+ 'maxrate': '6000k',
53
+ 'bufsize': '8000k',
54
+ 'audio_bitrate': '192k',
55
+ 'keyframe': '48',
56
+ 'preset': 'slow',
57
+ 'profile': 'high',
58
+ 'level': '4.0',
59
+ 'tune': 'fastdecode'
60
+ }
61
+ }
62
+ self.active_processes = {}
63
+
64
+ def start_encode_job(self, filename, job_id, output_name=None, settings=None):
65
+ """Start an encoding job with optional custom settings and output name"""
66
+ try:
67
+ qualities = self.default_qualities.copy()
68
+ if settings:
69
+ settings_dict = json.loads(settings)
70
+ for quality, bitrate in settings_dict.items():
71
+ if quality in qualities and bitrate:
72
+ qualities[quality]['bitrate'] = bitrate
73
+ # Adjust maxrate and bufsize based on new bitrate
74
+ bitrate_value = int(bitrate.replace('k', ''))
75
+ qualities[quality]['maxrate'] = f"{int(bitrate_value * 1.5)}k"
76
+ qualities[quality]['bufsize'] = f"{int(bitrate_value * 2)}k"
77
+
78
+ self.jobs[job_id] = {
79
+ 'status': 'pending',
80
+ 'progress': 0,
81
+ 'start_time': datetime.now().isoformat(),
82
+ 'filename': filename,
83
+ 'output_name': output_name or os.path.splitext(filename)[0],
84
+ 'current_quality': None,
85
+ 'outputs': [],
86
+ 'settings': qualities
87
+ }
88
+
89
+ # Start encoding in a separate thread
90
+ thread = threading.Thread(
91
+ target=self._encode_video,
92
+ args=(filename, job_id)
93
+ )
94
+ thread.daemon = True
95
+ thread.start()
96
+
97
+ return {'status': 'pending', 'job_id': job_id}
98
+ except Exception as e:
99
+ logger.error(f"Failed to start encoding job: {str(e)}")
100
+ return {'status': 'failed', 'error': str(e)}
101
+
102
+ def _encode_video(self, filename, job_id):
103
+ """Internal method to handle video encoding"""
104
+ try:
105
+ upload_path = Path(os.getenv('UPLOAD_FOLDER', 'uploads'))
106
+ encoded_path = Path(os.getenv('ENCODED_FOLDER', 'encoded'))
107
+ input_file = upload_path / filename
108
+ output_dir = encoded_path / job_id
109
+ output_name = self.jobs[job_id]['output_name']
110
+
111
+ # Create output directory
112
+ output_dir.mkdir(parents=True, exist_ok=True)
113
+
114
+ qualities = self.jobs[job_id]['settings']
115
+ total_steps = len(qualities)
116
+ completed_steps = 0
117
+ outputs = []
118
+
119
+ # Get video duration
120
+ duration = self._get_video_duration(input_file)
121
+ if not duration:
122
+ raise Exception("Could not determine video duration")
123
+
124
+ for quality, settings in qualities.items():
125
+ try:
126
+ self.jobs[job_id].update({
127
+ 'status': 'processing',
128
+ 'current_quality': quality,
129
+ 'progress': (completed_steps / total_steps) * 100
130
+ })
131
+
132
+ output_file = output_dir / f"{output_name}_{quality}.mp4"
133
+
134
+ # FFmpeg command with optimized settings for web streaming
135
+ cmd = [
136
+ 'ffmpeg', '-y',
137
+ '-i', str(input_file),
138
+ '-c:v', 'libx264',
139
+ '-preset', settings['preset'],
140
+ '-profile:v', settings['profile'],
141
+ '-level', settings['level'],
142
+ '-tune', settings['tune'],
143
+ '-b:v', settings['bitrate'],
144
+ '-maxrate', settings['maxrate'],
145
+ '-bufsize', settings['bufsize'],
146
+ '-vf', f"scale={settings['width']}:{settings['height']}",
147
+ '-g', settings['keyframe'],
148
+ '-keyint_min', settings['keyframe'],
149
+ '-sc_threshold', '0', # Disable scene cut detection
150
+ '-c:a', 'aac',
151
+ '-b:a', settings['audio_bitrate'],
152
+ '-ar', '48000', # Audio sample rate
153
+ '-ac', '2', # Stereo audio
154
+ '-movflags', '+faststart', # Enable fast start for web playback
155
+ '-progress', 'pipe:1',
156
+ str(output_file)
157
+ ]
158
+
159
+ process = subprocess.Popen(
160
+ cmd,
161
+ stdout=subprocess.PIPE,
162
+ stderr=subprocess.PIPE,
163
+ universal_newlines=True
164
+ )
165
+
166
+ self.active_processes[job_id] = process
167
+
168
+ # Monitor FFmpeg progress
169
+ while True:
170
+ output = process.stdout.readline()
171
+ if output == '' and process.poll() is not None:
172
+ break
173
+ if output:
174
+ progress = self._parse_ffmpeg_progress(output, duration)
175
+ if progress is not None:
176
+ quality_progress = (completed_steps + progress/100) / total_steps * 100
177
+ self.jobs[job_id]['progress'] = quality_progress
178
+
179
+ if process.returncode == 0:
180
+ outputs.append({
181
+ 'quality': quality,
182
+ 'path': str(output_file),
183
+ 'settings': settings
184
+ })
185
+ completed_steps += 1
186
+ else:
187
+ error_output = process.stderr.read()
188
+ logger.error(f"FFmpeg error: {error_output}")
189
+ raise Exception(f"FFmpeg failed for quality {quality}")
190
+
191
+ except Exception as e:
192
+ logger.error(f"Error encoding {quality}: {str(e)}")
193
+ self.jobs[job_id].update({
194
+ 'status': 'failed',
195
+ 'error': str(e)
196
+ })
197
+ return
198
+
199
+ finally:
200
+ if job_id in self.active_processes:
201
+ del self.active_processes[job_id]
202
+
203
+ self.jobs[job_id].update({
204
+ 'status': 'completed',
205
+ 'progress': 100,
206
+ 'outputs': outputs,
207
+ 'completion_time': datetime.now().isoformat()
208
+ })
209
+
210
+ except Exception as e:
211
+ logger.error(f"Encoding failed: {str(e)}")
212
+ self.jobs[job_id].update({
213
+ 'status': 'failed',
214
+ 'error': str(e)
215
+ })
216
+
217
+ def _get_video_duration(self, input_file):
218
+ """Get video duration using FFprobe"""
219
+ try:
220
+ cmd = [
221
+ 'ffprobe',
222
+ '-v', 'error',
223
+ '-show_entries', 'format=duration',
224
+ '-of', 'json',
225
+ str(input_file)
226
+ ]
227
+ result = subprocess.run(cmd, capture_output=True, text=True)
228
+ data = json.loads(result.stdout)
229
+ return float(data['format']['duration'])
230
+ except Exception as e:
231
+ logger.error(f"Error getting video duration: {str(e)}")
232
+ return None
233
+
234
+ def _parse_ffmpeg_progress(self, output, duration):
235
+ """Parse FFmpeg progress output"""
236
+ try:
237
+ if 'out_time_ms=' in output:
238
+ time_ms = int(output.split('out_time_ms=')[1].strip())
239
+ progress = (time_ms / 1000000) / duration * 100
240
+ return min(100, max(0, progress))
241
+ return None
242
+ except Exception:
243
+ return None
244
+
245
+ def stop_job(self, job_id):
246
+ """Stop an encoding job"""
247
+ try:
248
+ if job_id in self.active_processes:
249
+ process = self.active_processes[job_id]
250
+ process.send_signal(signal.SIGTERM)
251
+ process.wait(timeout=5)
252
+ del self.active_processes[job_id]
253
+ self.jobs[job_id]['status'] = 'stopped'
254
+ return True
255
+ return False
256
+ except Exception as e:
257
+ logger.error(f"Error stopping job {job_id}: {str(e)}")
258
+ return False
259
+
260
+ def clean_job(self, job_id):
261
+ """Clean up all files related to a job"""
262
+ try:
263
+ # Get paths
264
+ upload_path = Path(os.getenv('UPLOAD_FOLDER', 'uploads'))
265
+ encoded_path = Path(os.getenv('ENCODED_FOLDER', 'encoded'))
266
+
267
+ # Clean up source file if it exists
268
+ if job_id in self.jobs:
269
+ source_file = upload_path / self.jobs[job_id]['filename']
270
+ if source_file.exists():
271
+ source_file.unlink()
272
+
273
+ # Clean up encoded files
274
+ job_output_dir = encoded_path / job_id
275
+ if job_output_dir.exists():
276
+ shutil.rmtree(job_output_dir)
277
+
278
+ # Remove job from jobs dict
279
+ if job_id in self.jobs:
280
+ del self.jobs[job_id]
281
+
282
+ return True
283
+ except Exception as e:
284
+ logger.error(f"Error cleaning job {job_id}: {str(e)}")
285
+ return False
286
+
287
+ def get_job_info(self, job_id):
288
+ """Get detailed information about a job"""
289
+ try:
290
+ if job_id not in self.jobs:
291
+ return None
292
+
293
+ job = self.jobs[job_id]
294
+ encoded_path = Path(os.getenv('ENCODED_FOLDER', 'encoded'))
295
+ job_path = encoded_path / job_id
296
+
297
+ # Get file sizes
298
+ files_info = []
299
+ if job_path.exists():
300
+ for output in job.get('outputs', []):
301
+ file_path = Path(output['path'])
302
+ if file_path.exists():
303
+ files_info.append({
304
+ 'quality': output['quality'],
305
+ 'size': file_path.stat().st_size,
306
+ 'path': str(file_path)
307
+ })
308
+
309
+ return {
310
+ **job,
311
+ 'files': files_info
312
+ }
313
+ except Exception as e:
314
+ logger.error(f"Error getting job info: {str(e)}")
315
+ return None
316
+
317
+ encoder_service = EncoderService()
app/static/css/style.css ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles */
2
+ .upload-zone {
3
+ transition: all 0.3s ease;
4
+ }
5
+
6
+ .upload-zone:hover {
7
+ background-color: rgba(79, 70, 229, 0.05);
8
+ }
9
+
10
+ .progress-bar {
11
+ transition: width 0.3s ease;
12
+ }
13
+
14
+ .video-preview {
15
+ aspect-ratio: 16/9;
16
+ background: #000;
17
+ }
18
+
19
+ .modal-backdrop {
20
+ backdrop-filter: blur(2px);
21
+ }
22
+
23
+ .status-badge {
24
+ transition: all 0.3s ease;
25
+ }
26
+
27
+ .job-row {
28
+ transition: background-color 0.2s ease;
29
+ }
30
+
31
+ .job-row:hover {
32
+ background-color: rgba(79, 70, 229, 0.05);
33
+ }
34
+
35
+ .quality-button {
36
+ transition: all 0.2s ease;
37
+ }
38
+
39
+ .quality-button:hover {
40
+ transform: translateY(-1px);
41
+ }
app/static/favicon.ico ADDED
app/templates/files.html ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Files - Media Encoder Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ dark: {
15
+ 900: '#0f172a',
16
+ 800: '#1e293b',
17
+ 700: '#334155',
18
+ 600: '#475569',
19
+ 500: '#64748b'
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
27
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
28
+ <style>
29
+ body { font-family: 'Inter', sans-serif; background-color: #0f172a; }
30
+ </style>
31
+ </head>
32
+ <body class="text-gray-100">
33
+ <nav class="bg-dark-800 border-b border-dark-700">
34
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
35
+ <div class="flex justify-between h-16">
36
+ <div class="flex items-center">
37
+ <i class="fas fa-video text-indigo-500 text-2xl mr-2"></i>
38
+ <h1 class="text-xl font-semibold">Media Encoder Dashboard</h1>
39
+ </div>
40
+ <div class="flex items-center space-x-4">
41
+ <a href="/" class="text-gray-300 hover:text-gray-100">Home</a>
42
+ <a href="/files" class="text-indigo-400 hover:text-indigo-300">Files</a>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </nav>
47
+
48
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
49
+ <div class="bg-dark-800 border border-dark-700 shadow rounded-lg p-6">
50
+ <h2 class="text-lg font-medium mb-4">Encoded Files</h2>
51
+ <div id="fileList" class="space-y-4">
52
+ <!-- Files will be listed here -->
53
+ </div>
54
+ </div>
55
+ </main>
56
+
57
+ <!-- Video Preview Modal -->
58
+ <div id="videoModal" class="hidden fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm flex items-center justify-center z-50">
59
+ <div class="bg-dark-800 border border-dark-700 rounded-lg p-6 max-w-4xl w-full mx-4">
60
+ <div class="flex justify-between items-center mb-4">
61
+ <h3 class="text-lg font-medium">Video Preview</h3>
62
+ <button onclick="closeVideoModal()" class="text-gray-400 hover:text-gray-300">
63
+ <i class="fas fa-times"></i>
64
+ </button>
65
+ </div>
66
+ <div class="aspect-w-16 aspect-h-9">
67
+ <video id="videoPlayer" controls class="w-full rounded-lg">
68
+ Your browser does not support the video tag.
69
+ </video>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Confirmation Modal -->
75
+ <div id="confirmModal" class="hidden fixed inset-0 bg-black bg-opacity-75 backdrop-blur-sm flex items-center justify-center z-50">
76
+ <div class="bg-dark-800 border border-dark-700 rounded-lg p-6 max-w-md w-full mx-4">
77
+ <h3 class="text-lg font-medium mb-4">Confirm Deletion</h3>
78
+ <p class="text-gray-300 mb-6">Are you sure you want to delete this job and all its associated files? This action cannot be undone.</p>
79
+ <div class="flex justify-end space-x-3">
80
+ <button onclick="closeConfirmModal()" class="px-4 py-2 bg-dark-600 text-gray-300 rounded hover:bg-dark-500 transition-colors">
81
+ Cancel
82
+ </button>
83
+ <button id="confirmDeleteBtn" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors">
84
+ Delete
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <script>
91
+ function formatFileSize(bytes) {
92
+ if (bytes === 0) return '0 Bytes';
93
+ const k = 1024;
94
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
95
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
96
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
97
+ }
98
+
99
+ function formatDate(dateString) {
100
+ const date = new Date(dateString);
101
+ return date.toLocaleString();
102
+ }
103
+
104
+ async function loadFiles() {
105
+ try {
106
+ const response = await fetch('/api/files');
107
+ const data = await response.json();
108
+
109
+ const fileList = document.getElementById('fileList');
110
+ fileList.innerHTML = '';
111
+
112
+ data.files.forEach(file => {
113
+ const fileCard = document.createElement('div');
114
+ fileCard.className = 'bg-dark-700 rounded-lg p-4 hover:bg-dark-600 transition-colors duration-200';
115
+ fileCard.innerHTML = `
116
+ <div class="flex justify-between items-start">
117
+ <div class="space-y-1">
118
+ <h3 class="text-lg font-medium text-gray-100">${file.output_name}</h3>
119
+ <p class="text-sm text-gray-400">Job ID: ${file.job_id}</p>
120
+ <p class="text-sm text-gray-400">Created: ${formatDate(file.created_at)}</p>
121
+ ${file.completed_at ? `<p class="text-sm text-gray-400">Completed: ${formatDate(file.completed_at)}</p>` : ''}
122
+ </div>
123
+ <div class="flex items-start space-x-4">
124
+ <div class="flex flex-col items-end space-y-2">
125
+ ${Object.entries(file.qualities).map(([quality, size]) => `
126
+ <div class="flex items-center space-x-2">
127
+ <button onclick="playVideo('${file.job_id}', '${quality}', '${file.output_name}')"
128
+ class="text-xs bg-dark-600 hover:bg-dark-500 px-3 py-1.5 rounded transition-all duration-300">
129
+ <i class="fas fa-play mr-1"></i>${quality}
130
+ </button>
131
+ <a href="/video/${file.job_id}/${quality}"
132
+ download="${file.output_name}_${quality}.mp4"
133
+ class="text-xs bg-indigo-900 hover:bg-indigo-800 text-indigo-300 px-2 py-1.5 rounded transition-all duration-300">
134
+ <i class="fas fa-download"></i>
135
+ </a>
136
+ <span class="text-xs text-gray-400">${formatFileSize(size)}</span>
137
+ </div>
138
+ `).join('')}
139
+ </div>
140
+ <button onclick="showDeleteConfirmation('${file.job_id}')"
141
+ class="text-xs bg-red-900 hover:bg-red-800 text-red-300 px-3 py-1.5 rounded transition-all duration-300">
142
+ <i class="fas fa-trash"></i>
143
+ </button>
144
+ </div>
145
+ </div>
146
+ `;
147
+ fileList.appendChild(fileCard);
148
+ });
149
+ } catch (error) {
150
+ console.error('Error loading files:', error);
151
+ }
152
+ }
153
+
154
+ function playVideo(jobId, quality, outputName) {
155
+ const videoPlayer = document.getElementById('videoPlayer');
156
+ const videoModal = document.getElementById('videoModal');
157
+ videoPlayer.src = `/video/${jobId}/${quality}`;
158
+ videoModal.classList.remove('hidden');
159
+ }
160
+
161
+ function closeVideoModal() {
162
+ const videoModal = document.getElementById('videoModal');
163
+ const videoPlayer = document.getElementById('videoPlayer');
164
+ videoModal.classList.add('hidden');
165
+ videoPlayer.pause();
166
+ videoPlayer.src = '';
167
+ }
168
+
169
+ let jobToDelete = null;
170
+
171
+ function showDeleteConfirmation(jobId) {
172
+ jobToDelete = jobId;
173
+ document.getElementById('confirmModal').classList.remove('hidden');
174
+ }
175
+
176
+ function closeConfirmModal() {
177
+ jobToDelete = null;
178
+ document.getElementById('confirmModal').classList.add('hidden');
179
+ }
180
+
181
+ document.getElementById('confirmDeleteBtn').addEventListener('click', async () => {
182
+ if (!jobToDelete) return;
183
+
184
+ try {
185
+ const response = await fetch(`/api/jobs/${jobToDelete}/clean`, {
186
+ method: 'POST'
187
+ });
188
+
189
+ if (response.ok) {
190
+ closeConfirmModal();
191
+ loadFiles();
192
+ } else {
193
+ alert('Failed to delete job');
194
+ }
195
+ } catch (error) {
196
+ console.error('Error deleting job:', error);
197
+ alert('Error deleting job');
198
+ }
199
+ });
200
+
201
+ // Close modals when clicking outside
202
+ window.addEventListener('click', (e) => {
203
+ const videoModal = document.getElementById('videoModal');
204
+ const confirmModal = document.getElementById('confirmModal');
205
+ if (e.target === videoModal) closeVideoModal();
206
+ if (e.target === confirmModal) closeConfirmModal();
207
+ });
208
+
209
+ // Load files on page load
210
+ loadFiles();
211
+
212
+ // Refresh files list every 30 seconds
213
+ setInterval(loadFiles, 30000);
214
+ </script>
215
+ </body>
216
+ </html>
app/templates/index.html ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Media Encoder Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ dark: {
15
+ 900: '#0f172a',
16
+ 800: '#1e293b',
17
+ 700: '#334155',
18
+ 600: '#475569',
19
+ 500: '#64748b'
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
27
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
28
+ <style>
29
+ body { font-family: 'Inter', sans-serif; background-color: #0f172a; }
30
+ .drag-over { border-color: #6366f1 !important; background-color: rgba(99, 102, 241, 0.1) !important; }
31
+ .upload-progress { transition: width 0.3s ease-in-out; }
32
+ </style>
33
+ </head>
34
+ <body class="text-gray-100">
35
+ <nav class="bg-dark-800 border-b border-dark-700">
36
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
37
+ <div class="flex justify-between h-16">
38
+ <div class="flex items-center">
39
+ <i class="fas fa-video text-indigo-500 text-2xl mr-2"></i>
40
+ <h1 class="text-xl font-semibold">Media Encoder Dashboard</h1>
41
+ </div>
42
+ <div class="flex items-center space-x-4">
43
+ <a href="/" class="text-indigo-400 hover:text-indigo-300">Home</a>
44
+ <a href="/files" class="text-gray-300 hover:text-gray-100">Files</a>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </nav>
49
+
50
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
51
+ <div class="bg-dark-800 border border-dark-700 shadow rounded-lg p-6 mb-6">
52
+ <h2 class="text-lg font-medium mb-4">Upload Video</h2>
53
+ <form id="uploadForm" class="space-y-4">
54
+ <div class="flex items-center justify-center w-full">
55
+ <label id="dropZone" class="flex flex-col w-full h-32 border-2 border-dashed border-dark-600 rounded-lg cursor-pointer hover:border-indigo-500 transition-all duration-300">
56
+ <div class="flex flex-col items-center justify-center pt-7">
57
+ <i class="fas fa-cloud-upload-alt text-3xl text-indigo-500 mb-2"></i>
58
+ <p class="text-sm text-gray-300">Drag and drop or click to select</p>
59
+ <p class="text-xs text-gray-400">Supported formats: MP4, MOV, AVI, MKV, WMV</p>
60
+ </div>
61
+ <input type="file" id="videoFile" name="video" class="hidden" accept=".mp4,.mov,.avi,.mkv,.wmv">
62
+ </label>
63
+ </div>
64
+ <div id="fileInfo" class="hidden space-y-2">
65
+ <p class="text-sm text-gray-300">Selected file: <span id="fileName" class="font-medium text-indigo-400"></span></p>
66
+ <div id="uploadProgress" class="hidden">
67
+ <div class="w-full bg-dark-600 rounded-full h-2">
68
+ <div class="upload-progress bg-indigo-600 h-2 rounded-full" style="width: 0%"></div>
69
+ </div>
70
+ <p class="text-xs text-gray-400 mt-1"><span id="uploadPercent">0</span>% uploaded</p>
71
+ </div>
72
+ </div>
73
+ <div class="space-y-4">
74
+ <div class="bg-dark-700 p-4 rounded-lg">
75
+ <h3 class="text-sm font-medium mb-4">Output Settings</h3>
76
+ <div class="space-y-4">
77
+ <div>
78
+ <label class="block text-sm font-medium text-gray-300 mb-2">Output Name</label>
79
+ <input type="text" id="outputName" placeholder="Enter output name (without extension)"
80
+ class="w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
81
+ <p class="text-xs text-gray-400 mt-1">Quality will be appended automatically (e.g., name_480p.mp4)</p>
82
+ </div>
83
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
84
+ <div>
85
+ <label class="block text-sm font-medium text-gray-300">480p Settings</label>
86
+ <input type="text" id="480p_bitrate" placeholder="1000k"
87
+ class="mt-1 block w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
88
+ </div>
89
+ <div>
90
+ <label class="block text-sm font-medium text-gray-300">720p Settings</label>
91
+ <input type="text" id="720p_bitrate" placeholder="2500k"
92
+ class="mt-1 block w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
93
+ </div>
94
+ <div>
95
+ <label class="block text-sm font-medium text-gray-300">1080p Settings</label>
96
+ <input type="text" id="1080p_bitrate" placeholder="5000k"
97
+ class="mt-1 block w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ <button type="submit" id="uploadButton" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-300">
103
+ <i class="fas fa-upload mr-2"></i>
104
+ Upload & Start Encoding
105
+ </button>
106
+ </div>
107
+ </form>
108
+ </div>
109
+
110
+ <div class="bg-dark-800 border border-dark-700 shadow rounded-lg p-6">
111
+ <h2 class="text-lg font-medium mb-4">Encoding Jobs</h2>
112
+ <div class="overflow-x-auto">
113
+ <table class="min-w-full divide-y divide-dark-700">
114
+ <thead class="bg-dark-700">
115
+ <tr>
116
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Job ID</th>
117
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Output Name</th>
118
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
119
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Progress</th>
120
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Current Quality</th>
121
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
122
+ </tr>
123
+ </thead>
124
+ <tbody id="jobsList" class="bg-dark-800 divide-y divide-dark-700">
125
+ <!-- Jobs will be inserted here -->
126
+ </tbody>
127
+ </table>
128
+ </div>
129
+ </div>
130
+ </main>
131
+
132
+ <script>
133
+ // File upload handling
134
+ const uploadForm = document.getElementById('uploadForm');
135
+ const videoFile = document.getElementById('videoFile');
136
+ const fileInfo = document.getElementById('fileInfo');
137
+ const fileName = document.getElementById('fileName');
138
+ const uploadProgress = document.getElementById('uploadProgress');
139
+ const uploadPercent = document.getElementById('uploadPercent');
140
+ const progressBar = document.querySelector('.upload-progress');
141
+ const jobsList = document.getElementById('jobsList');
142
+ const dropZone = document.getElementById('dropZone');
143
+ const outputName = document.getElementById('outputName');
144
+
145
+ // Drag and drop handling
146
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
147
+ dropZone.addEventListener(eventName, preventDefaults, false);
148
+ document.body.addEventListener(eventName, preventDefaults, false);
149
+ });
150
+
151
+ ['dragenter', 'dragover'].forEach(eventName => {
152
+ dropZone.addEventListener(eventName, highlight, false);
153
+ });
154
+
155
+ ['dragleave', 'drop'].forEach(eventName => {
156
+ dropZone.addEventListener(eventName, unhighlight, false);
157
+ });
158
+
159
+ dropZone.addEventListener('drop', handleDrop, false);
160
+
161
+ function preventDefaults(e) {
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ }
165
+
166
+ function highlight(e) {
167
+ dropZone.classList.add('drag-over');
168
+ }
169
+
170
+ function unhighlight(e) {
171
+ dropZone.classList.remove('drag-over');
172
+ }
173
+
174
+ function handleDrop(e) {
175
+ const dt = e.dataTransfer;
176
+ const files = dt.files;
177
+ if (files.length > 0) {
178
+ videoFile.files = files;
179
+ updateFileInfo(files[0]);
180
+ }
181
+ }
182
+
183
+ videoFile.addEventListener('change', (e) => {
184
+ const file = e.target.files[0];
185
+ if (file) {
186
+ updateFileInfo(file);
187
+ // Set default output name from file name (without extension)
188
+ const defaultName = file.name.replace(/\.[^/.]+$/, "");
189
+ outputName.value = defaultName;
190
+ }
191
+ });
192
+
193
+ function updateFileInfo(file) {
194
+ fileName.textContent = file.name;
195
+ fileInfo.classList.remove('hidden');
196
+ uploadProgress.classList.add('hidden');
197
+ progressBar.style.width = '0%';
198
+ uploadPercent.textContent = '0';
199
+ }
200
+
201
+ uploadForm.addEventListener('submit', async (e) => {
202
+ e.preventDefault();
203
+ const formData = new FormData();
204
+ formData.append('video', videoFile.files[0]);
205
+ formData.append('output_name', outputName.value || videoFile.files[0].name.replace(/\.[^/.]+$/, ""));
206
+
207
+ // Add encoding settings
208
+ const settings = {
209
+ '480p': document.getElementById('480p_bitrate').value || '1000k',
210
+ '720p': document.getElementById('720p_bitrate').value || '2500k',
211
+ '1080p': document.getElementById('1080p_bitrate').value || '5000k'
212
+ };
213
+ formData.append('settings', JSON.stringify(settings));
214
+
215
+ try {
216
+ uploadProgress.classList.remove('hidden');
217
+ const xhr = new XMLHttpRequest();
218
+
219
+ xhr.upload.addEventListener('progress', (e) => {
220
+ if (e.lengthComputable) {
221
+ const percent = Math.round((e.loaded / e.total) * 100);
222
+ progressBar.style.width = percent + '%';
223
+ uploadPercent.textContent = percent;
224
+ }
225
+ });
226
+
227
+ xhr.onload = function() {
228
+ if (xhr.status === 202) {
229
+ const response = JSON.parse(xhr.responseText);
230
+ alert('Upload successful! Job ID: ' + response.job_id);
231
+ uploadForm.reset();
232
+ fileInfo.classList.add('hidden');
233
+ fetchJobs();
234
+ } else {
235
+ alert('Upload failed: ' + xhr.responseText);
236
+ }
237
+ };
238
+
239
+ xhr.onerror = function() {
240
+ alert('Upload failed. Please try again.');
241
+ };
242
+
243
+ xhr.open('POST', '/api/upload', true);
244
+ xhr.send(formData);
245
+ } catch (error) {
246
+ alert('Error uploading file: ' + error.message);
247
+ }
248
+ });
249
+
250
+ async function fetchJobs() {
251
+ try {
252
+ const response = await fetch('/api/jobs');
253
+ const data = await response.json();
254
+
255
+ jobsList.innerHTML = '';
256
+
257
+ Object.entries(data.jobs).forEach(([jobId, job]) => {
258
+ const row = document.createElement('tr');
259
+ row.className = 'hover:bg-dark-700 transition-colors duration-200';
260
+ row.innerHTML = `
261
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">${jobId}</td>
262
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${job.output_name || '-'}</td>
263
+ <td class="px-6 py-4 whitespace-nowrap">
264
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusClass(job.status)}">
265
+ ${job.status}
266
+ </span>
267
+ </td>
268
+ <td class="px-6 py-4 whitespace-nowrap">
269
+ <div class="w-full bg-dark-600 rounded-full h-2.5">
270
+ <div class="bg-indigo-600 h-2.5 rounded-full transition-all duration-300" style="width: ${job.progress || 0}%"></div>
271
+ </div>
272
+ <span class="text-xs text-gray-400 mt-1">${Math.round(job.progress || 0)}%</span>
273
+ </td>
274
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
275
+ ${job.current_quality || '-'}
276
+ </td>
277
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
278
+ ${job.status === 'completed' ? getVideoButtons(jobId, job.outputs, job.output_name) : ''}
279
+ ${job.status === 'processing' ? `
280
+ <button onclick="stopJob('${jobId}')" class="text-xs bg-red-900 hover:bg-red-800 text-red-300 px-2 py-1 rounded transition-all duration-300">
281
+ Stop
282
+ </button>
283
+ ` : ''}
284
+ </td>
285
+ `;
286
+ jobsList.appendChild(row);
287
+ });
288
+ } catch (error) {
289
+ console.error('Error fetching jobs:', error);
290
+ }
291
+ }
292
+
293
+ function getStatusClass(status) {
294
+ const classes = {
295
+ 'completed': 'bg-green-900 text-green-300',
296
+ 'processing': 'bg-yellow-900 text-yellow-300',
297
+ 'failed': 'bg-red-900 text-red-300',
298
+ 'pending': 'bg-dark-600 text-gray-300',
299
+ 'stopped': 'bg-gray-900 text-gray-300'
300
+ };
301
+ return classes[status] || classes.pending;
302
+ }
303
+
304
+ function getVideoButtons(jobId, outputs, outputName) {
305
+ if (!outputs) return '';
306
+
307
+ return `
308
+ <div class="space-x-2">
309
+ ${outputs.map(output => `
310
+ <a href="/video/${jobId}/${output.quality}"
311
+ class="text-xs bg-dark-700 hover:bg-dark-600 px-2 py-1 rounded transition-all duration-300"
312
+ download="${outputName || 'video'}_${output.quality}.mp4">
313
+ ${output.quality}
314
+ </a>
315
+ `).join('')}
316
+ </div>
317
+ `;
318
+ }
319
+
320
+ async function stopJob(jobId) {
321
+ try {
322
+ const response = await fetch(`/api/jobs/${jobId}/stop`, {
323
+ method: 'POST'
324
+ });
325
+ if (response.ok) {
326
+ fetchJobs();
327
+ } else {
328
+ alert('Failed to stop job');
329
+ }
330
+ } catch (error) {
331
+ console.error('Error stopping job:', error);
332
+ }
333
+ }
334
+
335
+ // Initial jobs fetch
336
+ fetchJobs();
337
+
338
+ // Poll for updates every 2 seconds
339
+ setInterval(fetchJobs, 2000);
340
+ </script>
341
+ </body>
342
+ </html>
app/utils.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import os
3
+ from pathlib import Path
4
+ from werkzeug.utils import secure_filename
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def generate_job_id():
10
+ """Generate a unique job ID."""
11
+ return str(uuid.uuid4())
12
+
13
+ def allowed_file(filename, allowed_extensions):
14
+ """Check if the file extension is allowed."""
15
+ return Path(filename).suffix.lower() in allowed_extensions
16
+
17
+ def get_secure_filename(filename):
18
+ """Generate a secure filename while preserving the extension."""
19
+ name, ext = os.path.splitext(filename)
20
+ secure_name = secure_filename(name)
21
+ return f"{secure_name}{ext}"
22
+
23
+ def ensure_directory(directory):
24
+ """Ensure a directory exists, create if it doesn't."""
25
+ try:
26
+ Path(directory).mkdir(parents=True, exist_ok=True)
27
+ return True
28
+ except Exception as e:
29
+ logger.error(f"Failed to create directory {directory}: {str(e)}")
30
+ return False
31
+
32
+ def get_video_path(job_id, quality, base_dir):
33
+ """Generate the path for an encoded video file."""
34
+ return Path(base_dir) / job_id / f"{quality}.mp4"
35
+
36
+ def cleanup_job_files(job_id, upload_dir, encoded_dir):
37
+ """Clean up temporary files after job completion or failure."""
38
+ try:
39
+ # Remove uploaded file
40
+ upload_path = Path(upload_dir) / job_id
41
+ if upload_path.exists():
42
+ upload_path.unlink()
43
+
44
+ # Remove encoded files directory
45
+ encoded_path = Path(encoded_dir) / job_id
46
+ if encoded_path.exists():
47
+ for file in encoded_path.glob('*'):
48
+ file.unlink()
49
+ encoded_path.rmdir()
50
+
51
+ logger.info(f"Cleaned up files for job {job_id}")
52
+ return True
53
+ except Exception as e:
54
+ logger.error(f"Failed to cleanup files for job {job_id}: {str(e)}")
55
+ return False
56
+
57
+ def get_job_status_message(status_code):
58
+ """Convert status code to human-readable message."""
59
+ status_messages = {
60
+ 'pending': 'Job is queued for processing',
61
+ 'processing': 'Video encoding is in progress',
62
+ 'completed': 'Video encoding completed successfully',
63
+ 'failed': 'Video encoding failed',
64
+ 'invalid_file': 'Invalid file type uploaded'
65
+ }
66
+ return status_messages.get(status_code, 'Unknown status')
create_favicon.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+
3
+ # Simple video camera icon in ICO format
4
+ icon_data = """
5
+ AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAA
6
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
8
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVVUBVVVVBlVVVQZVVVUBAAAAAAAA
9
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVVVQFVVVUjVVVVaVVVVWlVVVUjVVVV
10
+ AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVVVVAVVVVSNVVVWUVVVVzVVVVc1VVVWU
11
+ VVVVJFVVVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVVUBVVVVJFVVVZRVVVXpVVVV/1VVVf9V
12
+ VVXpVVVVlFVVVSRVVVUBAAAAAAAAAAAAAAAAVVVVAVVVVQZVVVUjVVVVlFVVVelVVVX/VVVV/1VV
13
+ Vf9VVVX/VVVVk1VVVSNVVVUGVVVVAQAAAAAAAAAAVVVVBlVVVSlVVVWUVVVV6VVVVf9VVVX/VVVV
14
+ /1VVVf9VVVX/VVVVlFVVVSlVVVUGAAAAAAAAAAAAAAAAAAAAAFVVVQZVVVUjVVVVlFVVVelVVVX/
15
+ VVVV/1VVVf9VVVX/VVVVlFVVVSNVVVUGAAAAAAAAAAAAAAAAAAAAAAAAAABVVVUBVVVVJFVVVZRV
16
+ VVXpVVVV/1VVVf9VVVXpVVVVlFVVVSRVVVUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVV
17
+ VQFVVVUjVVVVlFVVVc1VVVXNVVVVlFVVVSNVVVUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
18
+ AAAAAABVVVUBVVVVIlVVVWlVVVVpVVVVIlVVVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
19
+ AAAAAAAAAAAAAAAAAFVVVQFVVVUGVVVVBlVVVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
20
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
21
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
22
+ AAA=
23
+ """
24
+
25
+ # Decode base64 and write to file
26
+ with open('app/static/favicon.ico', 'wb') as f:
27
+ f.write(base64.b64decode(icon_data.strip()))
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask==2.0.1
2
+ Werkzeug==2.0.1
3
+ requests==2.31.0
4
+ python-dotenv==1.0.0
start_service.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import subprocess
3
+ import sys
4
+ import os
5
+ import time
6
+ import signal
7
+ import argparse
8
+ from pathlib import Path
9
+ import logging
10
+ from app import create_app
11
+
12
+ # Configure logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class ServiceManager:
20
+ def __init__(self, tmp_dir="/tmp", port=5000, worker_name=None):
21
+ self.processes = {}
22
+ self.tmp_dir = Path(tmp_dir)
23
+ self.port = port
24
+ self.worker_name = worker_name or f"encoder_worker_{os.getpid()}"
25
+
26
+ # Ensure required directories exist
27
+ self.uploads_dir = self.tmp_dir / "uploads"
28
+ self.encoded_dir = self.tmp_dir / "encoded"
29
+ self.uploads_dir.mkdir(parents=True, exist_ok=True)
30
+ self.encoded_dir.mkdir(parents=True, exist_ok=True)
31
+
32
+ # Set environment variables
33
+ os.environ['UPLOAD_FOLDER'] = str(self.uploads_dir)
34
+ os.environ['ENCODED_FOLDER'] = str(self.encoded_dir)
35
+
36
+ def start_flask(self):
37
+ """Start Flask application"""
38
+ try:
39
+ app = create_app()
40
+ from werkzeug.serving import run_simple
41
+ run_simple('0.0.0.0', self.port, app, use_reloader=False)
42
+ return True
43
+ except Exception as e:
44
+ logger.error(f"Failed to start Flask: {e}")
45
+ return False
46
+
47
+ def start_all(self):
48
+ """Start all services"""
49
+ return self.start_flask()
50
+
51
+ def stop_all(self):
52
+ """Stop all services"""
53
+ for name, process in self.processes.items():
54
+ try:
55
+ process.terminate()
56
+ process.wait(timeout=5)
57
+ logger.info(f"Stopped {name}")
58
+ except Exception as e:
59
+ logger.error(f"Error stopping {name}: {e}")
60
+ process.kill()
61
+
62
+ def cleanup(self):
63
+ """Clean up temporary files"""
64
+ try:
65
+ logger.info("Cleanup completed")
66
+ except Exception as e:
67
+ logger.error(f"Cleanup error: {e}")
68
+
69
+ def signal_handler(signum, frame):
70
+ """Handle termination signals"""
71
+ logger.info("Received termination signal")
72
+ manager.stop_all()
73
+ manager.cleanup()
74
+ sys.exit(0)
75
+
76
+ if __name__ == "__main__":
77
+ parser = argparse.ArgumentParser(description="Media Encoder Service Manager")
78
+ parser.add_argument("--tmp-dir", default="/tmp/encoder", help="Temporary directory path")
79
+ parser.add_argument("--port", type=int, default=5000, help="Port for the service")
80
+ parser.add_argument("--worker-name", help="Custom name for worker instance")
81
+
82
+ args = parser.parse_args()
83
+
84
+ # Register signal handlers
85
+ signal.signal(signal.SIGTERM, signal_handler)
86
+ signal.signal(signal.SIGINT, signal_handler)
87
+
88
+ # Create and start service manager
89
+ manager = ServiceManager(
90
+ tmp_dir=args.tmp_dir,
91
+ port=args.port,
92
+ worker_name=args.worker_name
93
+ )
94
+
95
+ if manager.start_all():
96
+ logger.info("Service started successfully")
97
+ try:
98
+ # Keep the script running
99
+ while True:
100
+ time.sleep(1)
101
+ except KeyboardInterrupt:
102
+ logger.info("Shutting down...")
103
+ manager.stop_all()
104
+ manager.cleanup()
105
+ else:
106
+ logger.error("Failed to start service")
107
+ manager.stop_all()
108
+ manager.cleanup()
109
+ sys.exit(1)