NitinBot002 commited on
Commit
888716d
·
verified ·
1 Parent(s): 6c4cd68

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -548
app.py DELETED
@@ -1,548 +0,0 @@
1
- import os
2
- import asyncio
3
- import json
4
- import logging
5
- from datetime import datetime
6
- from pathlib import Path
7
- import tempfile
8
- import threading
9
- import uuid
10
- from io import StringIO
11
-
12
- # --- Flask and Jinja Imports ---
13
- from flask import Flask, render_template, request, redirect, url_for, flash, render_template_string
14
- from jinja2 import BaseLoader, TemplateNotFound
15
-
16
- # --- Core Workflow Imports (Embedded) ---
17
- from telethon import TelegramClient
18
- from googleapiclient.discovery import build
19
- from googleapiclient.errors import HttpError
20
- from googleapiclient.http import MediaFileUpload
21
- from google.auth.transport.requests import Request
22
- from google.oauth2.credentials import Credentials
23
- from google_auth_oauthlib.flow import InstalledAppFlow
24
- import firebase_admin
25
- from firebase_admin import credentials, db
26
-
27
- # --- Custom Jinja Loader for Embedded Templates ---
28
- class StringLoader(BaseLoader):
29
- """A Jinja loader that loads templates from a dictionary."""
30
- def __init__(self, templates):
31
- self.templates = templates
32
-
33
- def get_source(self, environment, template):
34
- if template in self.templates:
35
- source = self.templates[template]
36
- # No real file path, so return None and a lambda that always returns True for uptodate
37
- return source, None, lambda: True
38
- raise TemplateNotFound(template)
39
-
40
- # --- Flask App Setup ---
41
- app = Flask(__name__)
42
- app.secret_key = 'your_strong_secret_key_change_this_for_production' # Change this!
43
-
44
- # --- In-Memory Storage for Logs & Status ---
45
- global_logs = {}
46
- processing_status = {}
47
-
48
- # --- Custom Logging Handler for Web Capture ---
49
- class InMemoryHandler(logging.Handler):
50
- """Custom logging handler to capture logs in memory for a specific run."""
51
- def __init__(self, run_id):
52
- super().__init__()
53
- self.run_id = run_id
54
-
55
- def emit(self, record):
56
- log_entry = self.format(record)
57
- if self.run_id not in global_logs:
58
- global_logs[self.run_id] = []
59
- global_logs[self.run_id].append(log_entry)
60
-
61
- # --- Embedded Core Workflow Code ---
62
- workflow_logger = logging.getLogger('workflow_logger')
63
- workflow_logger.setLevel(logging.INFO)
64
- if not workflow_logger.handlers:
65
- workflow_logger.addHandler(logging.StreamHandler())
66
-
67
- class TelegramYouTubeWorkflow:
68
- def __init__(self, config_dict, logger_instance=None):
69
- self.config = config_dict
70
- self.logger = logger_instance if logger_instance else workflow_logger
71
- self.telegram_client = None
72
- self.youtube_service = None
73
- self.firebase_db = None
74
- self.download_dir = Path(self.config.get('download_directory', 'downloads'))
75
- self.download_dir.mkdir(exist_ok=True)
76
- self.SCOPES = ['https://www.googleapis.com/auth/youtube.upload']
77
-
78
- def setup_firebase(self, service_account_content, database_url):
79
- try:
80
- # Use a temporary file for the service account credentials
81
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp_service_account:
82
- tmp_service_account.write(service_account_content)
83
- tmp_service_account_path = tmp_service_account.name
84
-
85
- try:
86
- # Check if a Firebase app is already initialized
87
- if not firebase_admin._apps:
88
- cred = credentials.Certificate(tmp_service_account_path)
89
- firebase_admin.initialize_app(cred, {'databaseURL': database_url})
90
- self.firebase_db = db
91
- self.logger.info("Connected to Firebase successfully")
92
- finally:
93
- os.unlink(tmp_service_account_path)
94
- except Exception as e:
95
- self.logger.error(f"Failed to connect to Firebase: {e}")
96
- raise
97
-
98
- async def setup_telegram_client(self):
99
- telegram_config = self.config['telegram']
100
- self.telegram_client = TelegramClient(
101
- 'session',
102
- telegram_config['api_id'],
103
- telegram_config['api_hash']
104
- )
105
- await self.telegram_client.start(phone=telegram_config['phone_number'])
106
- self.logger.info("Connected to Telegram successfully")
107
-
108
- def setup_youtube_client(self, client_secrets_content, token_path):
109
- """Initialize YouTube API client using provided secrets and token path"""
110
- creds = None
111
- # CORRECTED: Check if the token file exists AND is not empty
112
- if os.path.exists(token_path) and os.path.getsize(token_path) > 0:
113
- try:
114
- creds = Credentials.from_authorized_user_file(token_path, self.SCOPES)
115
- except json.JSONDecodeError:
116
- # The file exists but is corrupted or empty. Treat as if it doesn't exist.
117
- self.logger.warning(f"Token file at {token_path} is corrupted. Will generate a new one.")
118
- creds = None
119
-
120
- # If credentials are invalid or don't exist, get new ones
121
- if not creds or not creds.valid:
122
- if creds and creds.expired and creds.refresh_token:
123
- self.logger.info("Refreshing expired YouTube token...")
124
- creds.refresh(Request())
125
- else:
126
- self.logger.info("No valid YouTube token found. Starting OAuth flow...")
127
- # Write client secrets to a temporary file for the flow
128
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp_secrets:
129
- tmp_secrets.write(client_secrets_content)
130
- tmp_secrets_path = tmp_secrets.name
131
-
132
- try:
133
- flow = InstalledAppFlow.from_client_secrets_file(
134
- tmp_secrets_path,
135
- self.SCOPES
136
- )
137
- # This will print a URL to the console where you need to authenticate
138
- creds = flow.run_local_server(port=0)
139
- finally:
140
- os.unlink(tmp_secrets_path) # Clean up temp secrets file
141
-
142
- # Save the new or refreshed credentials for the next run
143
- with open(token_path, 'w') as token:
144
- token.write(creds.to_json())
145
- self.logger.info(f"YouTube token saved to {token_path}")
146
-
147
- self.youtube_service = build('youtube', 'v3', credentials=creds)
148
- self.logger.info("YouTube API client initialized successfully")
149
-
150
- def is_video_processed(self, telegram_video_id, channel_username):
151
- try:
152
- ref = self.firebase_db.reference(f'processed_videos/{channel_username}/{telegram_video_id}')
153
- return ref.get() is not None
154
- except Exception as e:
155
- self.logger.error(f"Error checking processed video: {e}")
156
- return False
157
-
158
- def save_processed_video(self, telegram_video_id, channel_username, youtube_video_id=None, video_info=None):
159
- try:
160
- telegram_url = f"https://t.me/{channel_username.lstrip('@')}/{telegram_video_id}"
161
- document = {
162
- "telegram_video_id": telegram_video_id,
163
- "channel_username": channel_username,
164
- "telegram_url": telegram_url,
165
- "youtube_video_id": youtube_video_id,
166
- "processed_at": datetime.utcnow().isoformat(),
167
- "status": "uploaded" if youtube_video_id else "failed"
168
- }
169
- if video_info:
170
- document.update({
171
- "video_title": video_info.get('caption', '')[:100],
172
- "video_date": video_info.get('date').isoformat(),
173
- "video_size": getattr(video_info.get('video'), 'size', None),
174
- "video_duration": getattr(video_info.get('video'), 'duration', None)
175
- })
176
- ref = self.firebase_db.reference(f'processed_videos/{channel_username}/{telegram_video_id}')
177
- ref.set(document)
178
- self.logger.info(f"Saved processed video info to Firebase: {telegram_url}")
179
- except Exception as e:
180
- self.logger.error(f"Error saving processed video info: {e}")
181
-
182
- def get_processed_videos_stats(self):
183
- try:
184
- ref = self.firebase_db.reference('processed_videos')
185
- all_videos = ref.get()
186
- if not all_videos: return {"total": 0, "uploaded": 0, "failed": 0}
187
- total = 0
188
- uploaded = 0
189
- for channel in all_videos.values():
190
- for video in channel.values():
191
- total += 1
192
- if video.get("status") == "uploaded":
193
- uploaded += 1
194
- failed = total - uploaded
195
- self.logger.info(f"DB stats - Total: {total}, Uploaded: {uploaded}, Failed: {failed}")
196
- return {"total": total, "uploaded": uploaded, "failed": failed}
197
- except Exception as e:
198
- self.logger.error(f"Error getting stats: {e}")
199
- return None
200
-
201
- async def get_channel_videos(self, limit=10):
202
- channel_username = self.config['telegram']['channel_username']
203
- try:
204
- entity = await self.telegram_client.get_entity(channel_username)
205
- videos = []
206
- async for message in self.telegram_client.iter_messages(entity, limit=limit):
207
- if message.video and not self.is_video_processed(message.id, channel_username):
208
- videos.append({
209
- 'id': message.id, 'message': message, 'video': message.video,
210
- 'caption': message.text or '', 'date': message.date,
211
- 'telegram_url': f"https://t.me/{channel_username.lstrip('@')}/{message.id}"
212
- })
213
- self.logger.info(f"Found {len(videos)} new videos to process.")
214
- return videos
215
- except Exception as e:
216
- self.logger.error(f"Error getting channel videos: {e}")
217
- return []
218
-
219
- async def download_video(self, video_info):
220
- try:
221
- filename = f"video_{video_info['id']}.mp4"
222
- filepath = self.download_dir / filename
223
- self.logger.info(f"Downloading video {video_info['id']}...")
224
- await video_info['message'].download_media(file=str(filepath))
225
- self.logger.info(f"Downloaded to: {filepath}")
226
- return str(filepath)
227
- except Exception as e:
228
- self.logger.error(f"Error downloading video {video_info['id']}: {e}")
229
- return None
230
-
231
- def upload_to_youtube(self, video_path, video_info):
232
- try:
233
- video_settings = self.config['video_settings']
234
- title = f"{video_settings.get('title_prefix', '')}{video_info['caption'][:100]}" or f"Video from Telegram {video_info['date'].strftime('%Y-%m-%d')}"
235
- body = {
236
- 'snippet': {
237
- 'title': title,
238
- 'description': f"{video_settings.get('description_template', '')}\n\n{video_info['caption']}",
239
- 'tags': video_settings.get('tags', []),
240
- 'categoryId': video_settings.get('category_id', '22')
241
- },
242
- 'status': {'privacyStatus': video_settings.get('privacy_status', 'private')}
243
- }
244
- media = MediaFileUpload(video_path, chunksize=-1, resumable=True)
245
- request = self.youtube_service.videos().insert(part=','.join(body.keys()), body=body, media_body=media)
246
-
247
- self.logger.info(f"Uploading {os.path.basename(video_path)} to YouTube...")
248
- response = None
249
- while response is None:
250
- status, response = request.next_chunk()
251
- if status:
252
- self.logger.info(f"Upload progress: {int(status.progress() * 100)}%")
253
-
254
- self.logger.info(f"Video uploaded successfully! YouTube ID: {response['id']}")
255
- return response['id']
256
- except HttpError as e:
257
- self.logger.error(f"HTTP error during upload: {e.content}")
258
- return None
259
- except Exception as e:
260
- self.logger.error(f"Error uploading to YouTube: {e}")
261
- return None
262
-
263
- def cleanup_video(self, video_path):
264
- try:
265
- os.remove(video_path)
266
- self.logger.info(f"Cleaned up: {video_path}")
267
- except Exception as e:
268
- self.logger.error(f"Error cleaning up {video_path}: {e}")
269
-
270
- async def process_videos(self, limit=5):
271
- self.logger.info("Starting video processing workflow...")
272
- try:
273
- await self.setup_telegram_client()
274
- self.get_processed_videos_stats()
275
- videos = await self.get_channel_videos(limit)
276
- if not videos:
277
- self.logger.info("No new videos found.")
278
- return
279
-
280
- for video_info in videos:
281
- video_path = None
282
- youtube_id = None
283
- try:
284
- self.logger.info(f"Processing video: {video_info['telegram_url']}")
285
- video_path = await self.download_video(video_info)
286
- if video_path:
287
- youtube_id = self.upload_to_youtube(video_path, video_info)
288
- except Exception as e:
289
- self.logger.error(f"Unhandled error processing video {video_info['id']}: {e}")
290
- finally:
291
- self.save_processed_video(
292
- video_info['id'], self.config['telegram']['channel_username'],
293
- youtube_video_id=youtube_id, video_info=video_info
294
- )
295
- if video_path:
296
- self.cleanup_video(video_path)
297
- except Exception as e:
298
- self.logger.error(f"Core workflow error: {e}", exc_info=True)
299
- finally:
300
- if self.telegram_client and self.telegram_client.is_connected():
301
- await self.telegram_client.disconnect()
302
- self.logger.info("Workflow finished.")
303
-
304
-
305
- # --- Flask Threading Function ---
306
-
307
- def run_workflow_in_thread(run_id, config_data, client_secrets_content, service_account_content, num_videos, token_path):
308
- log_handler = InMemoryHandler(run_id)
309
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
310
- log_handler.setFormatter(formatter)
311
- workflow_logger.addHandler(log_handler)
312
-
313
- try:
314
- processing_status[run_id] = "Running..."
315
- with tempfile.TemporaryDirectory() as temp_dir:
316
- config_data['download_directory'] = os.path.join(temp_dir, 'downloads')
317
- os.makedirs(config_data['download_directory'], exist_ok=True)
318
-
319
- original_cwd = os.getcwd()
320
- try:
321
- os.chdir(temp_dir)
322
- workflow = TelegramYouTubeWorkflow(config_dict=config_data, logger_instance=workflow_logger)
323
- workflow.setup_firebase(service_account_content, config_data['firebase']['database_url'])
324
- workflow.setup_youtube_client(client_secrets_content, token_path)
325
- asyncio.run(workflow.process_videos(limit=num_videos))
326
- processing_status[run_id] = "Completed."
327
- except Exception as e:
328
- processing_status[run_id] = f"Failed: {str(e)}"
329
- workflow_logger.error(f"Workflow thread failed: {e}", exc_info=True)
330
- finally:
331
- os.chdir(original_cwd)
332
- except Exception as e:
333
- processing_status[run_id] = f"Setup Failed: {str(e)}"
334
- workflow_logger.error(f"Thread setup failed: {e}", exc_info=True)
335
- finally:
336
- workflow_logger.removeHandler(log_handler)
337
- log_handler.close()
338
-
339
- # --- Flask Routes ---
340
-
341
- @app.route('/', methods=['GET', 'POST'])
342
- def index():
343
- if request.method == 'POST':
344
- try:
345
- # --- Firebase ---
346
- firebase_database_url = request.form['firebase_database_url']
347
- firebase_service_account_file = request.files['firebase_service_account']
348
- if not firebase_service_account_file or firebase_service_account_file.filename == '':
349
- flash('Firebase service account file is required.', 'error')
350
- return redirect(request.url)
351
- service_account_content = firebase_service_account_file.read().decode('utf-8')
352
-
353
- # --- YouTube ---
354
- client_secrets_file = request.files['youtube_client_secrets']
355
- if not client_secrets_file or client_secrets_file.filename == '':
356
- flash('YouTube client secrets file is required.', 'error')
357
- return redirect(request.url)
358
- client_secrets_content = client_secrets_file.read().decode('utf-8')
359
-
360
- config_data = {
361
- "telegram": {
362
- "api_id": int(request.form['telegram_api_id']),
363
- "api_hash": request.form['telegram_api_hash'],
364
- "phone_number": request.form['telegram_phone_number'],
365
- "channel_username": request.form['telegram_channel_username']
366
- },
367
- "firebase": { "database_url": firebase_database_url },
368
- "video_settings": {
369
- "title_prefix": request.form.get('title_prefix', ''),
370
- "description_template": request.form.get('description_template', 'Video from Telegram'),
371
- "tags": [tag.strip() for tag in request.form.get('tags', 'telegram,video').split(',') if tag.strip()],
372
- "category_id": request.form.get('category_id', '22'),
373
- "privacy_status": request.form.get('privacy_status', 'private')
374
- }
375
- }
376
- num_videos = int(request.form.get('num_videos', 5))
377
- run_id = str(uuid.uuid4())
378
- global_logs[run_id] = []
379
- processing_status[run_id] = "Starting..."
380
-
381
- token_fd, token_path = tempfile.mkstemp(suffix='.json', prefix=f'token_{run_id}_')
382
- os.close(token_fd) # Close descriptor, we only need the path
383
-
384
- thread = threading.Thread(
385
- target=run_workflow_in_thread,
386
- args=(run_id, config_data, client_secrets_content, service_account_content, num_videos, token_path)
387
- )
388
- thread.start()
389
-
390
- flash(f'Workflow started with ID: {run_id}', 'info')
391
- return redirect(url_for('results', run_id=run_id))
392
-
393
- except ValueError as e:
394
- flash(f'Invalid input data: {e}', 'error')
395
- return redirect(request.url)
396
- except Exception as e:
397
- flash(f'Error starting workflow: {e}', 'error')
398
- app.logger.error(f"Form processing error: {e}", exc_info=True)
399
- return redirect(request.url)
400
-
401
- return render_template('index.html')
402
-
403
- @app.route('/results/<run_id>')
404
- def results(run_id):
405
- logs = global_logs.get(run_id, [])
406
- status = processing_status.get(run_id, "Not Found")
407
- return render_template('results.html', run_id=run_id, logs=logs, status=status)
408
-
409
-
410
- # --- HTML Templates as Strings ---
411
-
412
- index_template_content = '''<!doctype html>
413
- <html lang="en">
414
- <head>
415
- <meta charset="utf-8">
416
- <meta name="viewport" content="width=device-width, initial-scale=1">
417
- <title>Telegram to YouTube Uploader</title>
418
- <style>
419
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }
420
- .container { max-width: 800px; margin: auto; background: #fff; padding: 2em; border-radius: 0.5em; box-shadow: 0 0 1em rgba(0,0,0,0.1); }
421
- h1, h2 { color: #007bff; }
422
- .form-group { margin-bottom: 1.5em; }
423
- label { display: block; margin-bottom: 0.5em; font-weight: bold; }
424
- input[type="text"], input[type="number"], input[type="password"], textarea, select {
425
- width: 100%; padding: 0.75em; box-sizing: border-box; border: 1px solid #ced4da; border-radius: 0.25em;
426
- }
427
- button { background-color: #007bff; color: white; padding: 0.75em 1.5em; border: none; cursor: pointer; border-radius: 0.25em; font-size: 1em; }
428
- button:hover { background-color: #0056b3; }
429
- .flash-messages { margin-bottom: 1.5em; }
430
- .flash-message { padding: 1em; margin-bottom: 1em; border-radius: 0.25em; }
431
- .flash-message-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
432
- .flash-message-error { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
433
- small { color: #6c757d; }
434
- </style>
435
- </head>
436
- <body>
437
- <div class="container">
438
- <h1>Telegram to YouTube Uploader</h1>
439
- <div class="flash-messages">
440
- {% with messages = get_flashed_messages(with_categories=true) %}
441
- {% if messages %}
442
- {% for category, message in messages %}
443
- <div class="flash-message flash-message-{{ category }}">{{ message }}</div>
444
- {% endfor %}
445
- {% endif %}
446
- {% endwith %}
447
- </div>
448
- <form method="post" enctype="multipart/form-data">
449
- <h2>Cloud Settings</h2>
450
- <div class="form-group">
451
- <label for="firebase_database_url">Firebase Realtime Database URL:</label>
452
- <input type="text" id="firebase_database_url" name="firebase_database_url" required placeholder="https://your-project-id-default-rtdb.firebaseio.com">
453
- </div>
454
- <div class="form-group">
455
- <label for="firebase_service_account">Firebase Service Account File (JSON):</label>
456
- <input type="file" id="firebase_service_account" name="firebase_service_account" accept=".json" required>
457
- <small>Download from Project Settings > Service accounts in Firebase.</small>
458
- </div>
459
- <div class="form-group">
460
- <label for="youtube_client_secrets">YouTube Client Secrets File (JSON):</label>
461
- <input type="file" id="youtube_client_secrets" name="youtube_client_secrets" accept=".json" required>
462
- <small>Download from Google Cloud > APIs & Services > Credentials (OAuth 2.0 Client ID for <strong>Desktop App</strong>).</small>
463
- </div>
464
-
465
- <h2>Telegram Settings</h2>
466
- <div class="form-group"><label for="telegram_api_id">API ID:</label><input type="number" id="telegram_api_id" name="telegram_api_id" required></div>
467
- <div class="form-group"><label for="telegram_api_hash">API Hash:</label><input type="text" id="telegram_api_hash" name="telegram_api_hash" required></div>
468
- <div class="form-group"><label for="telegram_phone_number">Phone Number (International):</label><input type="text" id="telegram_phone_number" name="telegram_phone_number" placeholder="+1234567890" required></div>
469
- <div class="form-group"><label for="telegram_channel_username">Channel Username:</label><input type="text" id="telegram_channel_username" name="telegram_channel_username" placeholder="@channelname" required></div>
470
-
471
- <h2>Processing Settings</h2>
472
- <div class="form-group"><label for="num_videos">Number of Recent Videos to Check:</label><input type="number" id="num_videos" name="num_videos" value="5" min="1"></div>
473
- <div class="form-group"><label for="title_prefix">YouTube Title Prefix:</label><input type="text" id="title_prefix" name="title_prefix"></div>
474
- <div class="form-group"><label for="description_template">Description Template:</label><textarea id="description_template" name="description_template">Video originally from Telegram.</textarea></div>
475
- <div class="form-group"><label for="tags">Tags (comma-separated):</label><input type="text" id="tags" name="tags" value="telegram,video,archive"></div>
476
- <div class="form-group">
477
- <label for="category_id">Category ID:</label><input type="text" id="category_id" name="category_id" value="22">
478
- <small><a href="https://developers.google.com/youtube/v3/docs/videoCategories/list" target="_blank">List of Category IDs</a></small>
479
- </div>
480
- <div class="form-group">
481
- <label for="privacy_status">Privacy Status:</label>
482
- <select id="privacy_status" name="privacy_status">
483
- <option value="private" selected>Private</option>
484
- <option value="public">Public</option>
485
- <option value="unlisted">Unlisted</option>
486
- </select>
487
- </div>
488
- <button type="submit">Start Workflow</button>
489
- </form>
490
- </div>
491
- </body>
492
- </html>
493
- '''
494
-
495
- results_template_content = '''<!doctype html>
496
- <html lang="en">
497
- <head>
498
- <meta charset="utf-8">
499
- <meta name="viewport" content="width=device-width, initial-scale=1">
500
- <title>Workflow Results - {{ run_id }}</title>
501
- <style>
502
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }
503
- .container { max-width: 900px; margin: auto; background: #fff; padding: 2em; border-radius: 0.5em; box-shadow: 0 0 1em rgba(0,0,0,0.1); }
504
- h1, h2 { color: #007bff; }
505
- pre { background-color: #e9ecef; padding: 1em; border: 1px solid #ced4da; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; border-radius: 0.25em; }
506
- .status { margin-bottom: 1.5em; padding: 1em; border-radius: 0.25em; font-weight: bold; }
507
- .status-Running { background-color: #fff3cd; border: 1px solid #ffeeba; color: #856404; }
508
- .status-Completed { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
509
- .status-Failed, .status-Setup { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
510
- .back-link { margin-top: 2em; display: inline-block; }
511
- .refresh-info { font-size: 0.9em; color: #6c757d; margin-bottom: 2em; }
512
- </style>
513
- </head>
514
- <body>
515
- <div class="container">
516
- <h1>Workflow Results</h1>
517
- <p><strong>Run ID:</strong> {{ run_id }}</p>
518
- <div class="status status-{{ status.split(':')[0] }}">
519
- <strong>Status:</strong> {{ status }}
520
- </div>
521
- <div class="refresh-info">
522
- <p>Page will auto-refresh every 5 seconds if the job is running.</p>
523
- </div>
524
- <h2>Logs:</h2>
525
- {% if logs %}
526
- <pre>{{ logs | join('\\n') }}</pre>
527
- {% else %}
528
- <p>No logs available yet. The process might be starting up.</p>
529
- {% endif %}
530
- <a href="{{ url_for('index') }}" class="back-link">Back to Start</a>
531
- </div>
532
- <script>
533
- (function() {
534
- const status = "{{ status }}";
535
- if (status.includes("Running") || status.includes("Starting")) {
536
- setTimeout(function() { location.reload(); }, 5000);
537
- }
538
- })();
539
- </script>
540
- </body>
541
- </html>
542
- '''
543
-
544
- # --- Use the custom StringLoader for our embedded templates ---
545
- app.jinja_loader = StringLoader({
546
- 'index.html': index_template_content,
547
- 'results.html': results_template_content,
548
- })