ChandimaPrabath commited on
Commit
c9b5d14
·
1 Parent(s): 75d8495

update debug

Browse files
Files changed (2) hide show
  1. app/routes.py +87 -0
  2. app/templates/index.html +389 -305
app/routes.py CHANGED
@@ -5,6 +5,8 @@ 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
 
@@ -115,6 +117,91 @@ def upload_video():
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"""
 
5
  import logging
6
  import json
7
  from datetime import datetime
8
+ import requests
9
+ from urllib.parse import urlparse
10
 
11
  from app.services.encoder_service import encoder_service
12
 
 
117
  'message': 'Failed to process upload'
118
  }), 500
119
 
120
+ @api_bp.route('/upload-url', methods=['POST'])
121
+ def upload_video_from_url():
122
+ """
123
+ Handle video file upload from a provided URL and start the encoding process.
124
+ Expects a JSON payload with:
125
+ - url: The URL of the video file.
126
+ - output_name: (optional) Desired output name without extension.
127
+ - settings: (optional) Custom encoding settings.
128
+ """
129
+ try:
130
+ data = request.get_json()
131
+ if not data or 'url' not in data:
132
+ return jsonify({
133
+ 'error': True,
134
+ 'message': 'No URL provided'
135
+ }), 400
136
+
137
+ video_url = data['url']
138
+ output_name = data.get('output_name')
139
+ settings = data.get('settings')
140
+
141
+ # Download the video file from the URL
142
+ response = requests.get(video_url, stream=True)
143
+ if response.status_code != 200:
144
+ return jsonify({
145
+ 'error': True,
146
+ 'message': 'Failed to download file from the provided URL'
147
+ }), 400
148
+
149
+ # Extract filename from URL
150
+ parsed_url = urlparse(video_url)
151
+ filename = os.path.basename(parsed_url.path)
152
+ if not filename:
153
+ # Fallback if filename is not present in the URL
154
+ content_type = response.headers.get('content-type', '')
155
+ extension = 'mp4'
156
+ if 'mov' in content_type:
157
+ extension = 'mov'
158
+ elif 'avi' in content_type:
159
+ extension = 'avi'
160
+ elif 'mkv' in content_type:
161
+ extension = 'mkv'
162
+ filename = f"downloaded_video.{extension}"
163
+
164
+ if not allowed_file(filename):
165
+ return jsonify({
166
+ 'error': True,
167
+ 'message': 'Invalid file type from URL'
168
+ }), 400
169
+
170
+ filename = secure_filename(filename)
171
+ file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
172
+
173
+ # Save the downloaded file in chunks
174
+ with open(file_path, 'wb') as f:
175
+ for chunk in response.iter_content(chunk_size=8192):
176
+ if chunk:
177
+ f.write(chunk)
178
+
179
+ # Set default output name if not provided
180
+ if not output_name:
181
+ output_name = filename.rsplit('.', 1)[0]
182
+
183
+ # Generate job ID and start encoding job
184
+ job_id = generate_job_id()
185
+ result = encoder_service.start_encode_job(
186
+ filename=filename,
187
+ job_id=job_id,
188
+ output_name=output_name,
189
+ settings=json.dumps(settings) if settings else None
190
+ )
191
+
192
+ return jsonify({
193
+ 'job_id': job_id,
194
+ 'message': 'Video download and encoding started',
195
+ 'status': result['status']
196
+ }), 202
197
+
198
+ except Exception as e:
199
+ logger.error(f"Video download from URL failed: {str(e)}")
200
+ return jsonify({
201
+ 'error': True,
202
+ 'message': 'Failed to process video from URL'
203
+ }), 500
204
+
205
  @api_bp.route('/jobs', methods=['GET'])
206
  def list_jobs():
207
  """List all encoding jobs"""
app/templates/index.html CHANGED
@@ -1,341 +1,425 @@
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
- uploadForm.reset();
231
- fileInfo.classList.add('hidden');
232
- fetchJobs();
233
- } else {
234
- alert('Upload failed: ' + xhr.responseText);
235
- }
236
- };
237
-
238
- xhr.onerror = function() {
239
- alert('Upload failed. Please try again.');
240
- };
241
-
242
- xhr.open('POST', '/api/upload', true);
243
- xhr.send(formData);
244
- } catch (error) {
245
- alert('Error uploading file: ' + error.message);
246
  }
247
- });
248
 
249
- async function fetchJobs() {
250
- try {
251
- const response = await fetch('/api/jobs');
252
- const data = await response.json();
253
-
254
- jobsList.innerHTML = '';
255
-
256
- Object.entries(data.jobs).forEach(([jobId, job]) => {
257
- const row = document.createElement('tr');
258
- row.className = 'hover:bg-dark-700 transition-colors duration-200';
259
- row.innerHTML = `
260
- <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">${jobId}</td>
261
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${job.output_name || '-'}</td>
262
- <td class="px-6 py-4 whitespace-nowrap">
263
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusClass(job.status)}">
264
- ${job.status}
265
- </span>
266
- </td>
267
- <td class="px-6 py-4 whitespace-nowrap">
268
- <div class="w-full bg-dark-600 rounded-full h-2.5">
269
- <div class="bg-indigo-600 h-2.5 rounded-full transition-all duration-300" style="width: ${job.progress || 0}%"></div>
270
- </div>
271
- <span class="text-xs text-gray-400 mt-1">${Math.round(job.progress || 0)}%</span>
272
- </td>
273
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
274
- ${job.current_quality || '-'}
275
- </td>
276
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
277
- ${job.status === 'completed' ? getVideoButtons(jobId, job.outputs, job.output_name) : ''}
278
- ${job.status === 'processing' ? `
279
- <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">
280
- Stop
281
- </button>
282
- ` : ''}
283
- </td>
284
- `;
285
- jobsList.appendChild(row);
286
- });
287
- } catch (error) {
288
- console.error('Error fetching jobs:', error);
289
  }
290
- }
291
 
292
- function getStatusClass(status) {
293
- const classes = {
294
- 'completed': 'bg-green-900 text-green-300',
295
- 'processing': 'bg-yellow-900 text-yellow-300',
296
- 'failed': 'bg-red-900 text-red-300',
297
- 'pending': 'bg-dark-600 text-gray-300',
298
- 'stopped': 'bg-gray-900 text-gray-300'
299
- };
300
- return classes[status] || classes.pending;
301
- }
302
 
303
- function getVideoButtons(jobId, outputs, outputName) {
304
- if (!outputs) return '';
305
-
306
- return `
307
- <div class="space-x-2">
308
- ${outputs.map(output => `
309
- <a href="/api/video/${jobId}/${output.quality}"
310
- class="text-xs bg-dark-700 hover:bg-dark-600 px-2 py-1 rounded transition-all duration-300"
311
- download="${outputName || 'video'}_${output.quality}.mp4">
312
- ${output.quality}
313
- </a>
314
- `).join('')}
315
- </div>
316
- `;
317
  }
 
 
 
 
 
318
 
319
- async function stopJob(jobId) {
320
- try {
321
- const response = await fetch(`/api/jobs/${jobId}/stop`, {
322
- method: 'POST'
323
- });
324
- if (response.ok) {
325
- fetchJobs();
326
- } else {
327
- alert('Failed to stop job');
328
- }
329
- } catch (error) {
330
- console.error('Error stopping job:', error);
331
- }
 
 
332
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
- // Initial jobs fetch
335
- fetchJobs();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- // Poll for updates every 2 seconds
338
- setInterval(fetchJobs, 2000);
339
- </script>
 
340
  </body>
341
  </html>
 
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
+ .tab-button {
33
+ padding: 0.5rem 1rem;
34
+ border: 1px solid #475569;
35
+ border-radius: 0.375rem;
36
+ cursor: pointer;
37
+ }
38
+ .tab-button.active {
39
+ background-color: #4f46e5;
40
+ color: white;
41
+ }
42
+ </style>
43
  </head>
44
  <body class="text-gray-100">
45
+ <nav class="bg-dark-800 border-b border-dark-700">
46
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
47
+ <div class="flex justify-between h-16">
48
+ <div class="flex items-center">
49
+ <i class="fas fa-video text-indigo-500 text-2xl mr-2"></i>
50
+ <h1 class="text-xl font-semibold">Media Encoder Dashboard</h1>
51
+ </div>
52
+ <div class="flex items-center space-x-4">
53
+ <a href="/" class="text-indigo-400 hover:text-indigo-300">Home</a>
54
+ <a href="/files" class="text-gray-300 hover:text-gray-100">Files</a>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </nav>
59
+
60
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
61
+ <!-- Upload Options Tabs -->
62
+ <div class="flex justify-center space-x-4 mb-6">
63
+ <button id="tabLocal" class="tab-button active">Upload from Device</button>
64
+ <button id="tabUrl" class="tab-button">Upload from URL</button>
65
+ </div>
66
+
67
+ <!-- Upload Form -->
68
+ <form id="uploadForm" class="bg-dark-800 border border-dark-700 shadow rounded-lg p-6 mb-6 space-y-6">
69
+ <!-- File Upload Section -->
70
+ <div id="localUploadSection">
71
+ <h2 class="text-lg font-medium mb-4">Upload Video from Device</h2>
72
+ <div class="flex items-center justify-center w-full">
73
+ <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">
74
+ <div class="flex flex-col items-center justify-center pt-7">
75
+ <i class="fas fa-cloud-upload-alt text-3xl text-indigo-500 mb-2"></i>
76
+ <p class="text-sm text-gray-300">Drag and drop or click to select</p>
77
+ <p class="text-xs text-gray-400">Supported formats: MP4, MOV, AVI, MKV, WMV</p>
78
+ </div>
79
+ <input type="file" id="videoFile" name="video" class="hidden" accept=".mp4,.mov,.avi,.mkv,.wmv">
80
+ </label>
81
+ </div>
82
+ <div id="fileInfo" class="hidden space-y-2">
83
+ <p class="text-sm text-gray-300">Selected file: <span id="fileName" class="font-medium text-indigo-400"></span></p>
84
+ <div id="uploadProgress" class="hidden">
85
+ <div class="w-full bg-dark-600 rounded-full h-2">
86
+ <div class="upload-progress bg-indigo-600 h-2 rounded-full" style="width: 0%"></div>
87
  </div>
88
+ <p class="text-xs text-gray-400 mt-1"><span id="uploadPercent">0</span>% uploaded</p>
89
+ </div>
90
  </div>
91
+ </div>
92
+
93
+ <!-- URL Upload Section -->
94
+ <div id="urlUploadSection" class="hidden">
95
+ <h2 class="text-lg font-medium mb-4">Upload Video from URL</h2>
96
+ <div>
97
+ <label for="videoUrl" class="block text-sm font-medium text-gray-300 mb-2">Video URL</label>
98
+ <input type="text" id="videoUrl" placeholder="Enter video URL" class="w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
+ </div>
101
 
102
+ <!-- Shared Settings Section -->
103
+ <div class="bg-dark-700 p-4 rounded-lg">
104
+ <h3 class="text-sm font-medium mb-4">Output Settings</h3>
105
+ <div class="space-y-4">
106
+ <div>
107
+ <label class="block text-sm font-medium text-gray-300 mb-2">Output Name</label>
108
+ <input type="text" id="outputName" placeholder="Enter output name (without extension)"
109
+ class="w-full px-3 py-2 bg-dark-600 border border-dark-500 rounded-md text-sm text-gray-100 placeholder-gray-400">
110
+ <p class="text-xs text-gray-400 mt-1">Quality will be appended automatically (e.g., name_480p.mp4)</p>
111
+ </div>
112
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
113
+ <div>
114
+ <label class="block text-sm font-medium text-gray-300">480p Settings</label>
115
+ <input type="text" id="480p_bitrate" placeholder="1000k"
116
+ 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">
117
+ </div>
118
+ <div>
119
+ <label class="block text-sm font-medium text-gray-300">720p Settings</label>
120
+ <input type="text" id="720p_bitrate" placeholder="2500k"
121
+ 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">
122
  </div>
123
+ <div>
124
+ <label class="block text-sm font-medium text-gray-300">1080p Settings</label>
125
+ <input type="text" id="1080p_bitrate" placeholder="5000k"
126
+ 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">
127
+ </div>
128
+ </div>
129
  </div>
130
+ </div>
131
+ <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">
132
+ <i class="fas fa-upload mr-2"></i>
133
+ Upload & Start Encoding
134
+ </button>
135
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ <!-- Encoding Jobs Table -->
138
+ <div class="bg-dark-800 border border-dark-700 shadow rounded-lg p-6">
139
+ <h2 class="text-lg font-medium mb-4">Encoding Jobs</h2>
140
+ <div class="overflow-x-auto">
141
+ <table class="min-w-full divide-y divide-dark-700">
142
+ <thead class="bg-dark-700">
143
+ <tr>
144
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Job ID</th>
145
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Output Name</th>
146
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
147
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Progress</th>
148
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Current Quality</th>
149
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
150
+ </tr>
151
+ </thead>
152
+ <tbody id="jobsList" class="bg-dark-800 divide-y divide-dark-700">
153
+ <!-- Jobs will be inserted here -->
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ </div>
158
+ </main>
159
 
160
+ <script>
161
+ let uploadMethod = 'local'; // default upload method
 
162
 
163
+ const tabLocal = document.getElementById('tabLocal');
164
+ const tabUrl = document.getElementById('tabUrl');
165
+ const localUploadSection = document.getElementById('localUploadSection');
166
+ const urlUploadSection = document.getElementById('urlUploadSection');
167
 
168
+ tabLocal.addEventListener('click', () => {
169
+ uploadMethod = 'local';
170
+ tabLocal.classList.add('active');
171
+ tabUrl.classList.remove('active');
172
+ localUploadSection.classList.remove('hidden');
173
+ urlUploadSection.classList.add('hidden');
174
+ });
175
 
176
+ tabUrl.addEventListener('click', () => {
177
+ uploadMethod = 'url';
178
+ tabUrl.classList.add('active');
179
+ tabLocal.classList.remove('active');
180
+ urlUploadSection.classList.remove('hidden');
181
+ localUploadSection.classList.add('hidden');
182
+ });
183
 
184
+ // File upload handling elements
185
+ const uploadForm = document.getElementById('uploadForm');
186
+ const videoFile = document.getElementById('videoFile');
187
+ const fileInfo = document.getElementById('fileInfo');
188
+ const fileName = document.getElementById('fileName');
189
+ const uploadProgress = document.getElementById('uploadProgress');
190
+ const uploadPercent = document.getElementById('uploadPercent');
191
+ const progressBar = document.querySelector('.upload-progress');
192
+ const jobsList = document.getElementById('jobsList');
193
+ const dropZone = document.getElementById('dropZone');
194
+ const outputName = document.getElementById('outputName');
195
 
196
+ // Drag and drop handling
197
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
198
+ dropZone.addEventListener(eventName, preventDefaults, false);
199
+ document.body.addEventListener(eventName, preventDefaults, false);
200
+ });
 
 
 
201
 
202
+ ['dragenter', 'dragover'].forEach(eventName => {
203
+ dropZone.addEventListener(eventName, highlight, false);
204
+ });
205
+
206
+ ['dragleave', 'drop'].forEach(eventName => {
207
+ dropZone.addEventListener(eventName, unhighlight, false);
208
+ });
 
 
209
 
210
+ dropZone.addEventListener('drop', handleDrop, false);
211
+
212
+ function preventDefaults(e) {
213
+ e.preventDefault();
214
+ e.stopPropagation();
215
+ }
216
+
217
+ function highlight(e) {
218
+ dropZone.classList.add('drag-over');
219
+ }
220
+
221
+ function unhighlight(e) {
222
+ dropZone.classList.remove('drag-over');
223
+ }
224
+
225
+ function handleDrop(e) {
226
+ const dt = e.dataTransfer;
227
+ const files = dt.files;
228
+ if (files.length > 0) {
229
+ videoFile.files = files;
230
+ updateFileInfo(files[0]);
231
+ }
232
+ }
233
+
234
+ videoFile.addEventListener('change', (e) => {
235
+ const file = e.target.files[0];
236
+ if (file) {
237
+ updateFileInfo(file);
238
+ // Set default output name from file name (without extension)
239
+ const defaultName = file.name.replace(/\.[^/.]+$/, "");
240
+ outputName.value = defaultName;
241
+ }
242
+ });
243
+
244
+ function updateFileInfo(file) {
245
+ fileName.textContent = file.name;
246
+ fileInfo.classList.remove('hidden');
247
+ uploadProgress.classList.add('hidden');
248
+ progressBar.style.width = '0%';
249
+ uploadPercent.textContent = '0';
250
+ }
251
+
252
+ uploadForm.addEventListener('submit', async (e) => {
253
+ e.preventDefault();
254
+
255
+ // Shared encoding settings and output name
256
+ const settings = {
257
+ '480p': document.getElementById('480p_bitrate').value || '1000k',
258
+ '720p': document.getElementById('720p_bitrate').value || '2500k',
259
+ '1080p': document.getElementById('1080p_bitrate').value || '5000k'
260
+ };
261
+
262
+ if (uploadMethod === 'local') {
263
+ // Local file upload
264
+ if (!videoFile.files[0]) {
265
+ alert('Please select a file to upload.');
266
+ return;
267
  }
268
+ const formData = new FormData();
269
+ formData.append('video', videoFile.files[0]);
270
+ formData.append('output_name', outputName.value || videoFile.files[0].name.replace(/\.[^/.]+$/, ""));
271
+ formData.append('settings', JSON.stringify(settings));
272
 
273
+ try {
274
+ uploadProgress.classList.remove('hidden');
275
+ const xhr = new XMLHttpRequest();
276
+ xhr.upload.addEventListener('progress', (e) => {
277
+ if (e.lengthComputable) {
278
+ const percent = Math.round((e.loaded / e.total) * 100);
279
+ progressBar.style.width = percent + '%';
280
+ uploadPercent.textContent = percent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  }
282
+ });
283
 
284
+ xhr.onload = function() {
285
+ if (xhr.status === 202) {
286
+ uploadForm.reset();
287
+ fileInfo.classList.add('hidden');
288
+ fetchJobs();
289
+ } else {
290
+ alert('Upload failed: ' + xhr.responseText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
292
+ };
293
 
294
+ xhr.onerror = function() {
295
+ alert('Upload failed. Please try again.');
296
+ };
 
 
 
 
 
 
 
297
 
298
+ xhr.open('POST', '/api/upload', true);
299
+ xhr.send(formData);
300
+ } catch (error) {
301
+ alert('Error uploading file: ' + error.message);
302
+ }
303
+ } else {
304
+ // URL upload
305
+ const videoUrl = document.getElementById('videoUrl').value.trim();
306
+ if (!videoUrl) {
307
+ alert('Please enter a video URL.');
308
+ return;
 
 
 
309
  }
310
+ const payload = {
311
+ url: videoUrl,
312
+ output_name: outputName.value || '',
313
+ settings: settings
314
+ };
315
 
316
+ try {
317
+ const response = await fetch('/api/upload-url', {
318
+ method: 'POST',
319
+ headers: { 'Content-Type': 'application/json' },
320
+ body: JSON.stringify(payload)
321
+ });
322
+ if (response.status === 202) {
323
+ uploadForm.reset();
324
+ fetchJobs();
325
+ } else {
326
+ const resText = await response.text();
327
+ alert('Upload failed: ' + resText);
328
+ }
329
+ } catch (error) {
330
+ alert('Error uploading video from URL: ' + error.message);
331
  }
332
+ }
333
+ });
334
+
335
+ async function fetchJobs() {
336
+ try {
337
+ const response = await fetch('/api/jobs');
338
+ const data = await response.json();
339
+
340
+ jobsList.innerHTML = '';
341
+
342
+ Object.entries(data.jobs).forEach(([jobId, job]) => {
343
+ const row = document.createElement('tr');
344
+ row.className = 'hover:bg-dark-700 transition-colors duration-200';
345
+ row.innerHTML = `
346
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">${jobId}</td>
347
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${job.output_name || '-'}</td>
348
+ <td class="px-6 py-4 whitespace-nowrap">
349
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusClass(job.status)}">
350
+ ${job.status}
351
+ </span>
352
+ </td>
353
+ <td class="px-6 py-4 whitespace-nowrap">
354
+ <div class="w-full bg-dark-600 rounded-full h-2.5">
355
+ <div class="bg-indigo-600 h-2.5 rounded-full transition-all duration-300" style="width: ${job.progress || 0}%"></div>
356
+ </div>
357
+ <span class="text-xs text-gray-400 mt-1">${Math.round(job.progress || 0)}%</span>
358
+ </td>
359
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
360
+ ${job.current_quality || '-'}
361
+ </td>
362
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
363
+ ${job.status === 'completed' ? getVideoButtons(jobId, job.outputs, job.output_name) : ''}
364
+ ${job.status === 'processing' ? `
365
+ <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">
366
+ Stop
367
+ </button>
368
+ ` : ''}
369
+ </td>
370
+ `;
371
+ jobsList.appendChild(row);
372
+ });
373
+ } catch (error) {
374
+ console.error('Error fetching jobs:', error);
375
+ }
376
+ }
377
 
378
+ function getStatusClass(status) {
379
+ const classes = {
380
+ 'completed': 'bg-green-900 text-green-300',
381
+ 'processing': 'bg-yellow-900 text-yellow-300',
382
+ 'failed': 'bg-red-900 text-red-300',
383
+ 'pending': 'bg-dark-600 text-gray-300',
384
+ 'stopped': 'bg-gray-900 text-gray-300'
385
+ };
386
+ return classes[status] || classes.pending;
387
+ }
388
+
389
+ function getVideoButtons(jobId, outputs, outputName) {
390
+ if (!outputs) return '';
391
+
392
+ return `
393
+ <div class="space-x-2">
394
+ ${outputs.map(output => `
395
+ <a href="/api/video/${jobId}/${output.quality}"
396
+ class="text-xs bg-dark-700 hover:bg-dark-600 px-2 py-1 rounded transition-all duration-300"
397
+ download="${outputName || 'video'}_${output.quality}.mp4">
398
+ ${output.quality}
399
+ </a>
400
+ `).join('')}
401
+ </div>
402
+ `;
403
+ }
404
+
405
+ async function stopJob(jobId) {
406
+ try {
407
+ const response = await fetch(`/api/jobs/${jobId}/stop`, {
408
+ method: 'POST'
409
+ });
410
+ if (response.ok) {
411
+ fetchJobs();
412
+ } else {
413
+ alert('Failed to stop job');
414
+ }
415
+ } catch (error) {
416
+ console.error('Error stopping job:', error);
417
+ }
418
+ }
419
 
420
+ // Initial jobs fetch and polling
421
+ fetchJobs();
422
+ setInterval(fetchJobs, 2000);
423
+ </script>
424
  </body>
425
  </html>