NitinBot002 commited on
Commit
543f020
·
verified ·
1 Parent(s): f20a123

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -548
app.py CHANGED
@@ -1,565 +1,238 @@
1
- import time
2
  import os
3
- import asyncio
4
  import json
5
- import logging
6
- from datetime import datetime
7
- from pathlib import Path
8
- import tempfile
9
- import threading
10
- import uuid
11
- from io import StringIO
12
-
13
- # --- Flask and Jinja Imports ---
14
- from flask import Flask, render_template, request, redirect, url_for, flash
15
- from jinja2 import BaseLoader, TemplateNotFound
16
-
17
- # --- Core Workflow Imports (Embedded) ---
18
- from telethon import TelegramClient
19
- from googleapiclient.discovery import build
20
- from googleapiclient.errors import HttpError
21
- from googleapiclient.http import MediaFileUpload
22
- from google.auth.transport.requests import Request
23
- from google.oauth2.credentials import Credentials
24
- from google_auth_oauthlib.flow import InstalledAppFlow
25
- import firebase_admin
26
- from firebase_admin import credentials, db
27
-
28
- # --- Custom Exception for Auth Flow ---
29
- class GoogleAuthError(Exception):
30
- pass
31
-
32
- # --- Custom Jinja Loader for Embedded Templates ---
33
- class StringLoader(BaseLoader):
34
- """A Jinja loader that loads templates from a dictionary."""
35
- def __init__(self, templates):
36
- self.templates = templates
37
 
38
- def get_source(self, environment, template):
39
- if template in self.templates:
40
- source = self.templates[template]
41
- return source, None, lambda: True
42
- raise TemplateNotFound(template)
43
 
44
- # --- Flask App Setup ---
45
  app = Flask(__name__)
46
- app.secret_key = 'your_strong_secret_key_change_this_for_production'
47
-
48
- # --- In-Memory Storage for Logs & Status ---
49
- global_logs = {}
50
- processing_status = {}
51
-
52
- # --- Custom Logging Handler for Web Capture ---
53
- class InMemoryHandler(logging.Handler):
54
- """Custom logging handler to capture logs in memory for a specific run."""
55
- def __init__(self, run_id):
56
- super().__init__()
57
- self.run_id = run_id
58
-
59
- def emit(self, record):
60
- log_entry = self.format(record)
61
- if self.run_id not in global_logs:
62
- global_logs[self.run_id] = []
63
- global_logs[self.run_id].append(log_entry)
64
-
65
- # --- Embedded Core Workflow Code ---
66
- workflow_logger = logging.getLogger('workflow_logger')
67
- workflow_logger.setLevel(logging.INFO)
68
- if not workflow_logger.handlers:
69
- workflow_logger.addHandler(logging.StreamHandler())
70
-
71
- class TelegramYouTubeWorkflow:
72
- def __init__(self, config_dict, logger_instance=None):
73
- self.config = config_dict
74
- self.logger = logger_instance if logger_instance else workflow_logger
75
- self.telegram_client = None
76
- self.youtube_service = None
77
- self.firebase_db = None
78
- self.download_dir = Path(self.config.get('download_directory', 'downloads'))
79
- self.download_dir.mkdir(exist_ok=True)
80
- self.SCOPES = ['https://www.googleapis.com/auth/youtube.upload']
81
-
82
- def setup_firebase(self, service_account_content, database_url):
83
- try:
84
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
85
- tmp.write(service_account_content)
86
- tmp_path = tmp.name
87
- try:
88
- if not firebase_admin._apps:
89
- cred = credentials.Certificate(tmp_path)
90
- firebase_admin.initialize_app(cred, {'databaseURL': database_url})
91
- self.firebase_db = db
92
- self.logger.info("Connected to Firebase successfully")
93
- finally:
94
- os.unlink(tmp_path)
95
- except Exception as e:
96
- self.logger.error(f"Failed to connect to Firebase: {e}")
97
- raise
98
-
99
- async def setup_telegram_client(self):
100
- # ... (rest of the class methods are unchanged)
101
- telegram_config = self.config['telegram']
102
- self.telegram_client = TelegramClient(
103
- 'session',
104
- telegram_config['api_id'],
105
- telegram_config['api_hash']
106
- )
107
- await self.telegram_client.start(phone=telegram_config['phone_number'])
108
- self.logger.info("Connected to Telegram successfully")
109
-
110
- def setup_youtube_client(self, client_secrets_content, token_path, auth_code=None):
111
- """Initialize YouTube API client using console flow for headless environments."""
112
- creds = None
113
- if os.path.exists(token_path) and os.path.getsize(token_path) > 0:
114
- creds = Credentials.from_authorized_user_file(token_path, self.SCOPES)
115
-
116
- if not creds or not creds.valid:
117
- if creds and creds.expired and creds.refresh_token:
118
- self.logger.info("Refreshing expired YouTube token...")
119
- creds.refresh(Request())
120
- else:
121
- self.logger.info("No valid YouTube token found. Attempting console auth flow.")
122
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
123
- tmp.write(client_secrets_content)
124
- secrets_path = tmp.name
125
-
126
- try:
127
- # Use the out-of-band (oob) redirect URI for the copy-paste flow
128
- flow = InstalledAppFlow.from_client_secrets_file(
129
- secrets_path, self.SCOPES, redirect_uri='urn:ietf:wg:oauth:2.0:oob'
130
- )
131
-
132
- if not auth_code:
133
- # If no code is provided, generate URL and stop
134
- auth_url, _ = flow.authorization_url(prompt='consent')
135
- self.logger.error("="*80)
136
- self.logger.error("USER ACTION REQUIRED FOR AUTHENTICATION:")
137
- self.logger.error(f"\n1. Open this URL in your browser:\n\n{auth_url}\n")
138
- self.logger.error("2. Authenticate and grant permissions.")
139
- self.logger.error("3. Copy the authorization code Google provides.")
140
- self.logger.error("4. Paste the code into the 'Google Auth Code' field on the main page and resubmit.")
141
- self.logger.error("="*80)
142
- time.sleep(30)
143
- else:
144
- # If code is provided, fetch the token
145
- self.logger.info("Authorization code provided. Fetching token...")
146
- flow.fetch_token(code=auth_code)
147
- creds = flow.credentials
148
- finally:
149
- os.unlink(secrets_path)
150
-
151
- # Save the new credentials
152
- with open(token_path, 'w') as token:
153
- token.write(creds.to_json())
154
- self.logger.info("YouTube token saved successfully.")
155
-
156
- self.youtube_service = build('youtube', 'v3', credentials=creds)
157
- self.logger.info("YouTube API client initialized successfully")
158
-
159
- def is_video_processed(self, telegram_video_id, channel_username):
160
- try:
161
- ref = self.firebase_db.reference(f'processed_videos/{channel_username}/{telegram_video_id}')
162
- return ref.get() is not None
163
- except Exception as e:
164
- self.logger.error(f"Error checking processed video: {e}")
165
- return False
166
-
167
- def save_processed_video(self, telegram_video_id, channel_username, youtube_video_id=None, video_info=None):
168
- try:
169
- telegram_url = f"https://t.me/{channel_username.lstrip('@')}/{telegram_video_id}"
170
- document = {
171
- "telegram_video_id": telegram_video_id,
172
- "channel_username": channel_username,
173
- "telegram_url": telegram_url,
174
- "youtube_video_id": youtube_video_id,
175
- "processed_at": datetime.utcnow().isoformat(),
176
- "status": "uploaded" if youtube_video_id else "failed"
177
- }
178
- if video_info:
179
- document.update({
180
- "video_title": video_info.get('caption', '')[:100],
181
- "video_date": video_info.get('date').isoformat(),
182
- "video_size": getattr(video_info.get('video'), 'size', None),
183
- "video_duration": getattr(video_info.get('video'), 'duration', None)
184
- })
185
- ref = self.firebase_db.reference(f'processed_videos/{channel_username}/{telegram_video_id}')
186
- ref.set(document)
187
- self.logger.info(f"Saved processed video info to Firebase: {telegram_url}")
188
- except Exception as e:
189
- self.logger.error(f"Error saving processed video info: {e}")
190
-
191
- def get_processed_videos_stats(self):
192
- try:
193
- ref = self.firebase_db.reference('processed_videos')
194
- all_videos = ref.get()
195
- if not all_videos: return {"total": 0, "uploaded": 0, "failed": 0}
196
- total = 0
197
- uploaded = 0
198
- for channel in all_videos.values():
199
- for video in channel.values():
200
- total += 1
201
- if video.get("status") == "uploaded":
202
- uploaded += 1
203
- failed = total - uploaded
204
- self.logger.info(f"DB stats - Total: {total}, Uploaded: {uploaded}, Failed: {failed}")
205
- return {"total": total, "uploaded": uploaded, "failed": failed}
206
- except Exception as e:
207
- self.logger.error(f"Error getting stats: {e}")
208
- return None
209
-
210
- async def get_channel_videos(self, limit=10):
211
- channel_username = self.config['telegram']['channel_username']
212
- try:
213
- entity = await self.telegram_client.get_entity(channel_username)
214
- videos = []
215
- async for message in self.telegram_client.iter_messages(entity, limit=limit):
216
- if message.video and not self.is_video_processed(message.id, channel_username):
217
- videos.append({
218
- 'id': message.id, 'message': message, 'video': message.video,
219
- 'caption': message.text or '', 'date': message.date,
220
- 'telegram_url': f"https://t.me/{channel_username.lstrip('@')}/{message.id}"
221
- })
222
- self.logger.info(f"Found {len(videos)} new videos to process.")
223
- return videos
224
- except Exception as e:
225
- self.logger.error(f"Error getting channel videos: {e}")
226
- return []
227
-
228
- async def download_video(self, video_info):
229
- try:
230
- filename = f"video_{video_info['id']}.mp4"
231
- filepath = self.download_dir / filename
232
- self.logger.info(f"Downloading video {video_info['id']}...")
233
- await video_info['message'].download_media(file=str(filepath))
234
- self.logger.info(f"Downloaded to: {filepath}")
235
- return str(filepath)
236
- except Exception as e:
237
- self.logger.error(f"Error downloading video {video_info['id']}: {e}")
238
- return None
239
-
240
- def upload_to_youtube(self, video_path, video_info):
241
- try:
242
- video_settings = self.config['video_settings']
243
- title = f"{video_settings.get('title_prefix', '')}{video_info['caption'][:100]}" or f"Video from Telegram {video_info['date'].strftime('%Y-%m-%d')}"
244
- body = {
245
- 'snippet': {
246
- 'title': title,
247
- 'description': f"{video_settings.get('description_template', '')}\n\n{video_info['caption']}",
248
- 'tags': video_settings.get('tags', []),
249
- 'categoryId': video_settings.get('category_id', '22')
250
- },
251
- 'status': {'privacyStatus': video_settings.get('privacy_status', 'private')}
252
- }
253
- media = MediaFileUpload(video_path, chunksize=-1, resumable=True)
254
- request = self.youtube_service.videos().insert(part=','.join(body.keys()), body=body, media_body=media)
255
-
256
- self.logger.info(f"Uploading {os.path.basename(video_path)} to YouTube...")
257
- response = None
258
- while response is None:
259
- status, response = request.next_chunk()
260
- if status:
261
- self.logger.info(f"Upload progress: {int(status.progress() * 100)}%")
262
-
263
- self.logger.info(f"Video uploaded successfully! YouTube ID: {response['id']}")
264
- return response['id']
265
- except HttpError as e:
266
- self.logger.error(f"HTTP error during upload: {e.content}")
267
- return None
268
- except Exception as e:
269
- self.logger.error(f"Error uploading to YouTube: {e}")
270
- return None
271
-
272
- def cleanup_video(self, video_path):
273
- try:
274
- os.remove(video_path)
275
- self.logger.info(f"Cleaned up: {video_path}")
276
- except Exception as e:
277
- self.logger.error(f"Error cleaning up {video_path}: {e}")
278
-
279
- async def process_videos(self, limit=5):
280
- self.logger.info("Starting video processing workflow...")
281
- try:
282
- await self.setup_telegram_client()
283
- self.get_processed_videos_stats()
284
- videos = await self.get_channel_videos(limit)
285
- if not videos:
286
- self.logger.info("No new videos found.")
287
- return
288
-
289
- for video_info in videos:
290
- video_path = None
291
- youtube_id = None
292
- try:
293
- self.logger.info(f"Processing video: {video_info['telegram_url']}")
294
- video_path = await self.download_video(video_info)
295
- if video_path:
296
- youtube_id = self.upload_to_youtube(video_path, video_info)
297
- except Exception as e:
298
- self.logger.error(f"Unhandled error processing video {video_info['id']}: {e}")
299
- finally:
300
- self.save_processed_video(
301
- video_info['id'], self.config['telegram']['channel_username'],
302
- youtube_video_id=youtube_id, video_info=video_info
303
- )
304
- if video_path:
305
- self.cleanup_video(video_path)
306
- except Exception as e:
307
- self.logger.error(f"Core workflow error: {e}", exc_info=True)
308
- finally:
309
- if self.telegram_client and self.telegram_client.is_connected():
310
- await self.telegram_client.disconnect()
311
- self.logger.info("Workflow finished.")
312
-
313
- # --- Flask Threading Function ---
314
 
315
- def run_workflow_in_thread(run_id, config_data, client_secrets_content, service_account_content, num_videos, token_path, auth_code):
316
- log_handler = InMemoryHandler(run_id)
317
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
318
- log_handler.setFormatter(formatter)
319
- workflow_logger.addHandler(log_handler)
320
 
 
 
321
  try:
322
- processing_status[run_id] = "Running..."
323
- with tempfile.TemporaryDirectory() as temp_dir:
324
- config_data['download_directory'] = os.path.join(temp_dir, 'downloads')
325
- os.makedirs(config_data['download_directory'], exist_ok=True)
326
-
327
- original_cwd = os.getcwd()
328
- try:
329
- os.chdir(temp_dir)
330
- workflow = TelegramYouTubeWorkflow(config_dict=config_data, logger_instance=workflow_logger)
331
- workflow.setup_firebase(service_account_content, config_data['firebase']['database_url'])
332
- workflow.setup_youtube_client(client_secrets_content, token_path, auth_code)
333
- asyncio.run(workflow.process_videos(limit=num_videos))
334
- processing_status[run_id] = "Completed."
335
- except GoogleAuthError as e:
336
- processing_status[run_id] = f"Authentication Required: {e}"
337
- workflow_logger.error(f"Workflow stopped for user authentication.")
338
- except Exception as e:
339
- processing_status[run_id] = f"Failed: {str(e)}"
340
- workflow_logger.error(f"Workflow thread failed: {e}", exc_info=True)
341
- finally:
342
- os.chdir(original_cwd)
343
  except Exception as e:
344
- processing_status[run_id] = f"Setup Failed: {str(e)}"
345
- workflow_logger.error(f"Thread setup failed: {e}", exc_info=True)
346
- finally:
347
- workflow_logger.removeHandler(log_handler)
348
- log_handler.close()
349
 
350
- # --- Flask Routes ---
351
-
352
- @app.route('/', methods=['GET', 'POST'])
353
- def index():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  if request.method == 'POST':
355
- try:
356
- # --- Firebase ---
357
- firebase_database_url = request.form['firebase_database_url']
358
- firebase_service_account_file = request.files['firebase_service_account']
359
- if not firebase_service_account_file or firebase_service_account_file.filename == '':
360
- flash('Firebase service account file is required.', 'error')
361
- return redirect(request.url)
362
- service_account_content = firebase_service_account_file.read().decode('utf-8')
363
-
364
- # --- YouTube ---
365
- client_secrets_file = request.files['youtube_client_secrets']
366
- if not client_secrets_file or client_secrets_file.filename == '':
367
- flash('YouTube client secrets file is required.', 'error')
368
- return redirect(request.url)
369
- client_secrets_content = client_secrets_file.read().decode('utf-8')
370
-
371
- # --- Google Auth Code (for first run) ---
372
- google_auth_code = request.form.get('google_auth_code', '').strip()
373
-
374
- config_data = {
375
- "telegram": {
376
- "api_id": int(request.form['telegram_api_id']),
377
- "api_hash": request.form['telegram_api_hash'],
378
- "phone_number": request.form['telegram_phone_number'],
379
- "channel_username": request.form['telegram_channel_username']
380
- },
381
- "firebase": { "database_url": firebase_database_url },
382
- "video_settings": {
383
- "title_prefix": request.form.get('title_prefix', ''),
384
- "description_template": request.form.get('description_template', 'Video from Telegram'),
385
- "tags": [tag.strip() for tag in request.form.get('tags', 'telegram,video').split(',') if tag.strip()],
386
- "category_id": request.form.get('category_id', '22'),
387
- "privacy_status": request.form.get('privacy_status', 'private')
388
- }
389
- }
390
- num_videos = int(request.form.get('num_videos', 5))
391
- run_id = str(uuid.uuid4())
392
- global_logs[run_id] = []
393
- processing_status[run_id] = "Starting..."
394
-
395
- token_fd, token_path = tempfile.mkstemp(suffix='.json', prefix='token_')
396
- os.close(token_fd)
397
-
398
- thread = threading.Thread(
399
- target=run_workflow_in_thread,
400
- args=(run_id, config_data, client_secrets_content, service_account_content, num_videos, token_path, google_auth_code)
401
- )
402
- thread.start()
403
-
404
- flash(f'Workflow started with ID: {run_id}', 'info')
405
- return redirect(url_for('results', run_id=run_id))
406
-
407
- except ValueError as e:
408
- flash(f'Invalid input data: {e}', 'error')
409
- return redirect(request.url)
410
- except Exception as e:
411
- flash(f'Error starting workflow: {e}', 'error')
412
- app.logger.error(f"Form processing error: {e}", exc_info=True)
413
- return redirect(request.url)
414
-
415
  return render_template('index.html')
416
 
417
- @app.route('/results/<run_id>')
418
- def results(run_id):
419
- logs = global_logs.get(run_id, [])
420
- status = processing_status.get(run_id, "Not Found")
421
- return render_template('results.html', run_id=run_id, logs=logs, status=status)
422
 
423
- # --- HTML Templates as Strings ---
424
-
425
- index_template_content = '''<!doctype html>
426
- <html lang="en">
427
- <head>
428
- <meta charset="utf-8">
429
- <meta name="viewport" content="width=device-width, initial-scale=1">
430
- <title>Telegram to YouTube Uploader</title>
431
- <style>
432
-
433
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }
434
- .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); }
435
- h1, h2 { color: #007bff; }
436
- h2.auth-header { color: #dc3545; }
437
- .form-group { margin-bottom: 1.5em; }
438
- label { display: block; margin-bottom: 0.5em; font-weight: bold; }
439
- input[type="text"], input[type="number"], input[type="password"], textarea, select {
440
- width: 100%; padding: 0.75em; box-sizing: border-box; border: 1px solid #ced4da; border-radius: 0.25em;
441
- }
442
- button { background-color: #007bff; color: white; padding: 0.75em 1.5em; border: none; cursor: pointer; border-radius: 0.25em; font-size: 1em; }
443
- button:hover { background-color: #0056b3; }
444
- .flash-messages { margin-bottom: 1.5em; }
445
- .flash-message { padding: 1em; margin-bottom: 1em; border-radius: 0.25em; }
446
- .flash-message-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
447
- .flash-message-error { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
448
- small { color: #6c757d; }
449
- .auth-box { border: 2px solid #fd7e14; padding: 1em; border-radius: 0.5em; margin-bottom: 1.5em; background-color: #fff3cd; }
450
- </style>
451
- </head>
452
- <body>
453
- <div class="container">
454
- <h1>Telegram to YouTube Uploader</h1>
455
- <div class="flash-messages">
456
- {% with messages = get_flashed_messages(with_categories=true) %}
457
- {% if messages %}
458
- {% for category, message in messages %}
459
- <div class="flash-message flash-message-{{ category }}">{{ message }}</div>
460
- {% endfor %}
461
- {% endif %}
462
- {% endwith %}
463
- </div>
464
- <form method="post" enctype="multipart/form-data">
465
- <div class="auth-box">
466
- <h2 class="auth-header">Google Authentication</h2>
467
- <div class="form-group">
468
- <label for="google_auth_code">Google Auth Code (only needed for first time setup)</label>
469
- <input type="text" id="google_auth_code" name="google_auth_code" placeholder="Paste code here if requested in logs">
470
- <small>If the logs ask for an auth code, paste it here and resubmit the form.</small>
471
- </div>
472
- </div>
473
-
474
- <h2>Cloud Settings</h2>
475
- <div class="form-group">
476
- <label for="firebase_database_url">Firebase Realtime Database URL:</label>
477
- <input type="text" id="firebase_database_url" name="firebase_database_url" required placeholder="https://your-project-id-default-rtdb.firebaseio.com">
478
- </div>
479
- <div class="form-group">
480
- <label for="firebase_service_account">Firebase Service Account File (JSON):</label>
481
- <input type="file" id="firebase_service_account" name="firebase_service_account" accept=".json" required>
482
- </div>
483
- <div class="form-group">
484
- <label for="youtube_client_secrets">YouTube Client Secrets File (JSON):</label>
485
- <input type="file" id="youtube_client_secrets" name="youtube_client_secrets" accept=".json" required>
486
- </div>
487
 
488
- <h2>Telegram Settings</h2>
489
- <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>
490
- <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>
491
- <div class="form-group"><label for="telegram_phone_number">Phone Number:</label><input type="text" id="telegram_phone_number" name="telegram_phone_number" placeholder="+1234567890" required></div>
492
- <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>
493
-
494
- <h2>Processing Settings</h2>
495
- <div class="form-group"><label for="num_videos">Videos to Check:</label><input type="number" id="num_videos" name="num_videos" value="5" min="1"></div>
496
- <div class="form-group"><label for="title_prefix">YouTube Title Prefix:</label><input type="text" id="title_prefix" name="title_prefix"></div>
497
- <div class="form-group"><label for="description_template">Description Template:</label><textarea id="description_template" name="description_template">Video from Telegram.</textarea></div>
498
- <div class="form-group"><label for="tags">Tags (comma-separated):</label><input type="text" id="tags" name="tags" value="telegram,video,archive"></div>
499
- <div class="form-group">
500
- <label for="category_id">Category ID:</label><input type="text" id="category_id" name="category_id" value="22">
501
- </div>
502
- <div class="form-group">
503
- <label for="privacy_status">Privacy Status:</label>
504
- <select id="privacy_status" name="privacy_status"><option value="private" selected>Private</option><option value="public">Public</option><option value="unlisted">Unlisted</option></select>
505
- </div>
506
- <button type="submit">Start Workflow</button>
507
- </form>
508
- </div>
509
- </body>
510
- </html>
511
- '''
512
 
513
- results_template_content = '''<!doctype html>
514
- <html lang="en">
515
- <head>
516
- <meta charset="utf-8">
517
- <meta name="viewport" content="width=device-width, initial-scale=1">
518
- <title>Workflow Results - {{ run_id }}</title>
519
- <style>
520
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }
521
- .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); }
522
- h1, h2 { color: #007bff; }
523
- 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; }
524
- .status { margin-bottom: 1.5em; padding: 1em; border-radius: 0.25em; font-weight: bold; }
525
- .status-Running { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
526
- .status-Completed { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
527
- .status-Failed, .status-Setup, .status-Authentication { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
528
- .back-link { margin-top: 2em; display: inline-block; }
529
- </style>
530
- </head>
531
- <body>
532
- <div class="container">
533
- <h1>Workflow Results</h1>
534
- <p><strong>Run ID:</strong> {{ run_id }}</p>
535
- <div class="status status-{{ status.split(':')[0] }}">
536
- <strong>Status:</strong> {{ status }}
537
- </div>
538
- <div class="refresh-info">
539
- <p>Page will auto-refresh every 5 seconds if the job is running.</p>
540
- </div>
541
- <h2>Logs:</h2>
542
- {% if logs %}
543
- <pre>{{ logs | join('\\n') }}</pre>
544
- {% else %}
545
- <p>No logs available yet. The process might be starting up.</p>
546
- {% endif %}
547
- <a href="{{ url_for('index') }}" class="back-link">Back to Start</a>
548
- </div>
549
- <script>
550
- (function() {
551
- const status = "{{ status }}";
552
- if (status.includes("Running") || status.includes("Starting")) {
553
- setTimeout(function() { location.reload(); }, 5000);
554
- }
555
- })();
556
- </script>
557
- </body>
558
- </html>
559
- '''
560
 
561
- # --- Use the custom StringLoader for our embedded templates ---
562
- app.jinja_loader = StringLoader({
563
- 'index.html': index_template_content,
564
- 'results.html': results_template_content,
565
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
2
  import json
3
+ import asyncio
4
+ from functools import wraps
5
+ from flask import Flask, request, redirect, session, url_for, render_template, jsonify, flash
6
+ from google_auth_oauthlib.flow import Flow
7
+ from telethon.sync import TelegramClient
8
+ from telethon.sessions import StringSession
9
+ from telethon.errors import SessionPasswordNeededError, PhoneCodeInvalidError, ApiIdInvalidError, ApiIdPublishedFloodError
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ from telegram_youtube_workflow import TelegramYouTubeWorkflow, logger
 
 
 
 
12
 
13
+ # Initialize Flask App
14
  app = Flask(__name__)
15
+ app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'a-strong-dev-secret-key-for-local-runs')
16
+
17
+ # --- Configuration & DB Logic ---
18
+ def setup_configuration():
19
+ """Loads secrets from environment variables and writes them to files at runtime."""
20
+ client_secrets_content = os.environ.get('CLIENT_SECRETS_CONTENT')
21
+ if client_secrets_content:
22
+ with open('client_secrets.json', 'w') as f: f.write(client_secrets_content)
23
+ logger.info("Successfully created client_secrets.json.")
24
+
25
+ firebase_key_content = os.environ.get('FIREBASE_KEY_CONTENT')
26
+ if firebase_key_content:
27
+ with open('firebase_service_account.json', 'w') as f: f.write(firebase_key_content)
28
+ logger.info("Successfully created firebase_service_account.json.")
29
+
30
+ # The session string will now be loaded from the database
31
+ session_string = load_telegram_session_from_db()
32
+
33
+ return {
34
+ "telegram": {
35
+ "api_id": os.environ.get('TELEGRAM_API_ID'),
36
+ "api_hash": os.environ.get('TELEGRAM_API_HASH'),
37
+ "session_string": session_string,
38
+ "channel_username": os.environ.get('TELEGRAM_CHANNEL_USERNAME')
39
+ },
40
+ "youtube": { "client_secrets_file": "client_secrets.json" },
41
+ "firebase": { "service_account_key": "firebase_service_account.json", "collection_name": os.environ.get('FIREBASE_COLLECTION_NAME', 'processed_videos') },
42
+ "download_directory": "downloads",
43
+ "video_settings": {
44
+ "title_prefix": os.environ.get('VIDEO_TITLE_PREFIX', ''), "description_template": os.environ.get('VIDEO_DESCRIPTION', 'Video from Telegram channel'),
45
+ "tags": os.environ.get('VIDEO_TAGS', 'telegram,video').split(','), "category_id": os.environ.get('VIDEO_CATEGORY_ID', '22'),
46
+ "privacy_status": os.environ.get('VIDEO_PRIVACY_STATUS', 'private')
47
+ }
48
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ workflow = TelegramYouTubeWorkflow(setup_configuration())
 
 
 
 
51
 
52
+ def save_telegram_session_to_db(session_string):
53
+ """Saves the Telethon session string to Firestore."""
54
  try:
55
+ if not workflow.firestore_db:
56
+ logger.error("Firestore is not initialized. Cannot save session.")
57
+ return
58
+ doc_ref = workflow.firestore_db.collection('app_config').document('telegram_credentials')
59
+ doc_ref.set({'session_string': session_string})
60
+ logger.info("Successfully saved Telethon session to Firestore.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  except Exception as e:
62
+ logger.error(f"Failed to save session to Firestore: {e}")
 
 
 
 
63
 
64
+ def load_telegram_session_from_db():
65
+ """Loads the Telethon session string from Firestore."""
66
+ try:
67
+ if not workflow.firestore_db: return None
68
+ doc_ref = workflow.firestore_db.collection('app_config').document('telegram_credentials')
69
+ doc = doc_ref.get()
70
+ if doc.exists:
71
+ logger.info("Loaded Telethon session from Firestore.")
72
+ return doc.to_dict().get('session_string')
73
+ return None
74
+ except Exception as e:
75
+ logger.error(f"Failed to load session from Firestore: {e}")
76
+ return None
77
+
78
+ # --- Login Protection Decorator ---
79
+ def login_required(f):
80
+ @wraps(f)
81
+ def decorated_function(*args, **kwargs):
82
+ if 'logged_in' not in session:
83
+ return redirect(url_for('login'))
84
+ return f(*args, **kwargs)
85
+ return decorated_function
86
+
87
+ # --- Web Routes ---
88
+
89
+ @app.route('/login', methods=['GET', 'POST'])
90
+ def login():
91
  if request.method == 'POST':
92
+ app_username = os.environ.get('APP_USERNAME')
93
+ app_password = os.environ.get('APP_PASSWORD')
94
+ if not app_username or not app_password:
95
+ flash('Application credentials are not configured on the server.', 'danger')
96
+ return render_template('login.html')
97
+ if request.form['username'] == app_username and request.form['password'] == app_password:
98
+ session['logged_in'] = True
99
+ return redirect(url_for('index'))
100
+ else:
101
+ flash('Invalid username or password.', 'danger')
102
+ return render_template('login.html')
103
+
104
+ @app.route('/')
105
+ @login_required
106
+ def index():
107
+ # Re-check configuration on every visit to the index
108
+ workflow.config = setup_configuration()
109
+ if not workflow.config['telegram']['session_string']:
110
+ return redirect(url_for('telegram_auth'))
111
+ if not os.path.exists('token.json'):
112
+ return redirect(url_for('youtube_auth'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  return render_template('index.html')
114
 
115
+ @app.route('/telegram-auth', methods=['GET', 'POST'])
116
+ @login_required
117
+ def telegram_auth():
118
+ stage = session.get('telegram_auth_stage', 'credentials')
 
119
 
120
+ if request.method == 'POST':
121
+ api_id = session.get('api_id') or request.form.get('api_id')
122
+ api_hash = session.get('api_hash') or request.form.get('api_hash')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ if not api_id or not api_hash:
125
+ flash("API ID and Hash are required.", "danger")
126
+ return render_template('telegram_auth.html', stage='credentials')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ session['api_id'] = api_id
129
+ session['api_hash'] = api_hash
130
+ client = TelegramClient(StringSession(), int(api_id), api_hash)
131
+ loop = asyncio.get_event_loop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ try:
134
+ async def do_login():
135
+ await client.connect()
136
+ if stage == 'credentials':
137
+ phone = request.form['phone']
138
+ session['phone'] = phone
139
+ result = await client.send_code_request(phone)
140
+ session['phone_code_hash'] = result.phone_code_hash
141
+ session['telegram_auth_stage'] = 'otp'
142
+ flash("An OTP code has been sent to your Telegram account.", "info")
143
+ elif stage == 'otp':
144
+ phone = session['phone']
145
+ phone_code_hash = session['phone_code_hash']
146
+ otp = request.form['otp']
147
+ try:
148
+ await client.sign_in(phone, otp, phone_code_hash=phone_code_hash)
149
+ final_session_string = client.session.save()
150
+ save_telegram_session_to_db(final_session_string)
151
+ session.pop('telegram_auth_stage', None)
152
+ flash("Telegram authentication successful!", "success")
153
+ except SessionPasswordNeededError:
154
+ session['telegram_auth_stage'] = 'password'
155
+ elif stage == 'password':
156
+ password = request.form['password']
157
+ await client.sign_in(password=password)
158
+ final_session_string = client.session.save()
159
+ save_telegram_session_to_db(final_session_string)
160
+ session.pop('telegram_auth_stage', None)
161
+ flash("Telegram authentication successful!", "success")
162
+
163
+ loop.run_until_complete(do_login())
164
+ except (ApiIdInvalidError, ApiIdPublishedFloodError):
165
+ flash("The API ID/Hash is invalid or blocked. Please check your credentials.", "danger")
166
+ session.pop('telegram_auth_stage', None)
167
+ except PhoneCodeInvalidError:
168
+ flash("Invalid OTP code. Please try again.", "danger")
169
+ session['telegram_auth_stage'] = 'otp' # Stay on OTP stage
170
+ except Exception as e:
171
+ flash(f"An error occurred: {str(e)}", "danger")
172
+ session.pop('telegram_auth_stage', None)
173
+ finally:
174
+ if client.is_connected():
175
+ loop.run_until_complete(client.disconnect())
176
+ return redirect(url_for('index'))
177
+
178
+ is_configured = bool(load_telegram_session_from_db())
179
+ if is_configured:
180
+ return redirect(url_for('index'))
181
+ return render_template('telegram_auth.html', stage=session.get('telegram_auth_stage', 'credentials'))
182
+
183
+ @app.route('/youtube-auth')
184
+ @login_required
185
+ def youtube_auth():
186
+ return render_template('auth.html')
187
+
188
+ @app.route('/authorize')
189
+ @login_required
190
+ def authorize():
191
+ if not os.path.exists('client_secrets.json'):
192
+ return "Error: YouTube client secrets are not configured on the server.", 500
193
+ flow = Flow.from_client_secrets_file(
194
+ 'client_secrets.json', scopes=workflow.SCOPES, redirect_uri=url_for('oauth2callback', _external=True)
195
+ )
196
+ authorization_url, state = flow.authorization_url(access_type='offline', include_granted_scopes='true', prompt='consent')
197
+ session['state'] = state
198
+ return redirect(authorization_url)
199
+
200
+ @app.route('/oauth2callback')
201
+ @login_required
202
+ def oauth2callback():
203
+ state = session.get('state')
204
+ if not state: return "Authentication error: session state is missing.", 400
205
+ flow = Flow.from_client_secrets_file(
206
+ 'client_secrets.json', scopes=workflow.SCOPES, state=state, redirect_uri=url_for('oauth2callback', _external=True)
207
+ )
208
+ flow.fetch_token(authorization_response=request.url)
209
+ with open('token.json', 'w') as token_file:
210
+ token_file.write(flow.credentials.to_json())
211
+ logger.info("Successfully received and stored YouTube API token.")
212
+ return redirect(url_for('index'))
213
+
214
+ @app.route('/start-workflow', methods=['POST'])
215
+ @login_required
216
+ def start_workflow():
217
+ if not os.path.exists('token.json'):
218
+ return jsonify({"error": "Not authenticated with YouTube. Please re-authorize."}), 401
219
+ limit = int(request.form.get('limit', 5))
220
+ offset = int(request.form.get('offset', 0))
221
+ workflow.build_youtube_service()
222
+ if not workflow.config['telegram']['session_string']:
223
+ return jsonify({"error": "Telegram session is not configured. Please re-authenticate."}), 401
224
+ loop = asyncio.get_event_loop()
225
+ result = loop.run_until_complete(workflow.process_single_batch(limit=limit, offset_id=offset))
226
+ return jsonify(result)
227
+
228
+ @app.route('/logout')
229
+ def logout():
230
+ session.clear()
231
+ if os.path.exists('token.json'):
232
+ os.remove('token.json')
233
+ flash('You have been successfully logged out.')
234
+ return redirect(url_for('login'))
235
+
236
+ if __name__ == '__main__':
237
+ port = int(os.environ.get('PORT', 10000))
238
+ app.run(host='0.0.0.0', port=port, debug=True)