NitinBot002 commited on
Commit
3f656d8
·
verified ·
1 Parent(s): 5c017d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +792 -195
app.py CHANGED
@@ -1,238 +1,835 @@
 
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', 7860))
238
- app.run(host='0.0.0.0', port=port, debug=True)
 
1
+ # app.py
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 time
11
  from functools import wraps
12
+ from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify
13
+ from werkzeug.security import check_password_hash, generate_password_hash
14
+ import secrets
15
+ # Required libraries
16
+ from telethon import TelegramClient, errors
17
+ from googleapiclient.discovery import build
18
+ from googleapiclient.errors import HttpError
19
+ from googleapiclient.http import MediaFileUpload
20
+ from google.auth.transport.requests import Request
21
+ from google.oauth2.credentials import Credentials
22
+ from google_auth_oauthlib.flow import InstalledAppFlow
23
+ import firebase_admin
24
+ from firebase_admin import credentials, firestore
25
+ # Configure logging
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(levelname)s - %(message)s'
29
+ )
30
+ logger = logging.getLogger(__name__)
31
  app = Flask(__name__)
32
+ app.secret_key = os.environ.get('FLASK_SECRET_KEY', secrets.token_hex(32))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # --- Add this section for User Configuration ---
35
+ USER_CONFIG_FILE = 'user_config.json'
36
 
37
+ def load_user_config():
38
+ """Load user configuration from file."""
39
+ config = {}
40
+ if os.path.exists(USER_CONFIG_FILE):
41
+ try:
42
+ with open(USER_CONFIG_FILE, 'r') as f:
43
+ config = json.load(f)
44
+ except Exception as e:
45
+ logger.error(f"Error loading user config: {e}")
46
+ return config
 
47
 
48
+ def save_user_config(config):
49
+ """Save user configuration to file."""
50
  try:
51
+ # Basic validation could be added here
52
+ with open(USER_CONFIG_FILE, 'w') as f:
53
+ json.dump(config, f, indent=4)
54
+ logger.info("User configuration saved successfully.")
 
 
 
55
  except Exception as e:
56
+ logger.error(f"Error saving user config: {e}")
57
+ raise # Re-raise to be handled by the route
58
+
59
+ # Global variables for workflow state
60
+ workflow_instance = None
61
+ processing_status = {
62
+ 'is_running': False,
63
+ 'current_batch': 0,
64
+ 'processed_count': 0,
65
+ 'failed_count': 0,
66
+ 'waiting_for_confirmation': False,
67
+ 'confirmation_message': '',
68
+ 'logs': []
69
+ }
70
+
71
+ class WebAppLogger:
72
+ """Custom logger to capture logs for web display"""
73
+ def __init__(self):
74
+ self.logs = []
75
+ self.max_logs = 100
76
+ def add_log(self, level, message):
77
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
78
+ log_entry = {
79
+ 'timestamp': timestamp,
80
+ 'level': level,
81
+ 'message': message
82
+ }
83
+ self.logs.append(log_entry)
84
+ if len(self.logs) > self.max_logs:
85
+ self.logs.pop(0)
86
+ # Also log to console
87
+ if level == 'INFO':
88
+ logger.info(message)
89
+ elif level == 'ERROR':
90
+ logger.error(message)
91
+ elif level == 'WARNING':
92
+ logger.warning(message)
93
+ def get_logs(self):
94
+ return self.logs
95
+ def clear_logs(self):
96
+ self.logs = []
97
+
98
+ web_logger = WebAppLogger()
99
+
100
+ class TelegramYouTubeWorkflow:
101
+ def __init__(self):
102
+ """Initialize the workflow with environment variables and user config"""
103
+ self.telegram_client = None
104
+ self.youtube_service = None
105
+ self.firestore_db = None
106
+ self.download_dir = Path(tempfile.gettempdir()) / 'telegram_downloads'
107
+ self.download_dir.mkdir(exist_ok=True)
108
+ # YouTube API scopes
109
+ self.SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube']
110
+ # --- Load User Configuration ---
111
+ self.user_config = load_user_config()
112
+ # Initialize Firebase
113
+ self.setup_firebase()
114
+
115
+ def create_file_from_env(self, env_var_name, filename):
116
+ """Create file from environment variable content"""
117
+ content = os.environ.get(env_var_name)
118
+ if not content:
119
+ raise ValueError(f"Environment variable {env_var_name} not found")
120
+ filepath = Path(filename)
121
+ with open(filepath, 'w') as f:
122
+ f.write(content)
123
+ web_logger.add_log('INFO', f"Created {filename} from environment variable")
124
+ return str(filepath)
125
+
126
+ def setup_firebase(self):
127
+ """Initialize Firebase connection"""
128
+ try:
129
+ # Create Firebase service account key from environment variable
130
+ service_account_path = self.create_file_from_env('FIREBASE_SERVICE_ACCOUNT_KEY', 'firebase_service_account.json')
131
+ # Initialize Firebase Admin SDK
132
+ if not firebase_admin._apps:
133
+ cred = credentials.Certificate(service_account_path)
134
+ firebase_admin.initialize_app(cred)
135
+ self.firestore_db = firestore.client()
136
+ self.collection_name = os.environ.get('FIREBASE_COLLECTION_NAME', 'processed_videos')
137
+ web_logger.add_log('INFO', "Firebase connection established successfully")
138
+ except Exception as e:
139
+ web_logger.add_log('ERROR', f"Error setting up Firebase: {e}")
140
+ raise
141
+
142
+ async def setup_telegram_client(self):
143
+ """Initialize and connect to Telegram"""
144
+ try:
145
+ api_id = os.environ.get('TELEGRAM_API_ID')
146
+ api_hash = os.environ.get('TELEGRAM_API_HASH')
147
+ phone_number = os.environ.get('TELEGRAM_PHONE_NUMBER')
148
+ if not all([api_id, api_hash, phone_number]):
149
+ raise ValueError("Missing Telegram credentials in environment variables")
150
+ self.telegram_client = TelegramClient('session', int(api_id), api_hash)
151
+ # Check if session exists, if not, we need OTP authentication
152
+ if not os.path.exists('session.session'):
153
+ web_logger.add_log('INFO', "No existing session found. Starting authentication...")
154
+ await self.telegram_client.start(phone=phone_number)
155
+ else:
156
+ await self.telegram_client.start()
157
+ web_logger.add_log('INFO', "Connected to Telegram successfully")
158
+ except Exception as e:
159
+ web_logger.add_log('ERROR', f"Error setting up Telegram client: {e}")
160
+ raise
161
+
162
+ def get_youtube_auth_url(self):
163
+ """Get YouTube OAuth authorization URL"""
164
+ try:
165
+ # Create client secrets file from environment variable
166
+ client_secrets_path = self.create_file_from_env('YOUTUBE_CLIENT_SECRETS', 'client_secrets.json')
167
+ # Create OAuth2 flow
168
+ flow = InstalledAppFlow.from_client_secrets_file(client_secrets_path, self.SCOPES)
169
+ flow.redirect_uri = 'http://127.0.0.1:8080'
170
+ # Generate authorization URL
171
+ auth_url, _ = flow.authorization_url(
172
+ prompt='consent',
173
+ access_type='offline',
174
+ include_granted_scopes='true'
175
+ )
176
+ # Store flow in session for later use
177
+ session['oauth_flow_state'] = flow.state
178
+ return auth_url
179
+ except Exception as e:
180
+ web_logger.add_log('ERROR', f"Error generating YouTube auth URL: {e}")
181
+ raise
182
+
183
+ def complete_youtube_auth(self, auth_code):
184
+ """Complete YouTube OAuth with authorization code"""
185
+ try:
186
+ client_secrets_path = self.create_file_from_env('YOUTUBE_CLIENT_SECRETS', 'client_secrets.json')
187
+ flow = InstalledAppFlow.from_client_secrets_file(client_secrets_path, self.SCOPES)
188
+ flow.redirect_uri = 'http://127.0.0.1:8080'
189
+ # Exchange code for credentials
190
+ flow.fetch_token(code=auth_code)
191
+ creds = flow.credentials
192
+ # Save credentials
193
+ with open('token.json', 'w') as token:
194
+ token.write(creds.to_json())
195
+ # Build YouTube service
196
+ self.youtube_service = build('youtube', 'v3', credentials=creds)
197
+ web_logger.add_log('INFO', "YouTube authentication completed successfully")
198
+ return True
199
+ except Exception as e:
200
+ web_logger.add_log('ERROR', f"Error completing YouTube authentication: {e}")
201
+ return False
202
+
203
+ def setup_youtube_client(self):
204
+ """Initialize YouTube API client"""
205
+ try:
206
+ creds = None
207
+ token_file = 'token.json'
208
+ # Load existing credentials
209
+ if os.path.exists(token_file):
210
+ creds = Credentials.from_authorized_user_file(token_file, self.SCOPES)
211
+ # Check if credentials are valid
212
+ if not creds or not creds.valid:
213
+ if creds and creds.expired and creds.refresh_token:
214
+ try:
215
+ creds.refresh(Request())
216
+ web_logger.add_log('INFO', "Refreshed existing YouTube credentials")
217
+ except Exception as e:
218
+ web_logger.add_log('ERROR', f"Failed to refresh credentials: {e}")
219
+ return False
220
+ else:
221
+ web_logger.add_log('WARNING', "No valid YouTube credentials found. Please authenticate.")
222
+ return False
223
+ self.youtube_service = build('youtube', 'v3', credentials=creds)
224
+ web_logger.add_log('INFO', "YouTube API client initialized successfully")
225
+ return True
226
+ except Exception as e:
227
+ web_logger.add_log('ERROR', f"Error setting up YouTube client: {e}")
228
+ return False
229
+
230
+ def is_video_processed(self, channel_username, message_id):
231
+ """Check if video is already processed using Firebase"""
232
+ try:
233
+ doc_id = f"{channel_username}_{message_id}"
234
+ doc_ref = self.firestore_db.collection(self.collection_name).document(doc_id)
235
+ doc = doc_ref.get()
236
+ return doc.exists
237
+ except Exception as e:
238
+ web_logger.add_log('ERROR', f"Error checking processed video: {e}")
239
+ return False
240
+
241
+ def mark_video_processed(self, channel_username, message_id, youtube_id=None, telegram_url=None):
242
+ """Mark video as processed in Firebase"""
243
+ try:
244
+ doc_id = f"{channel_username}_{message_id}"
245
+ doc_data = {
246
+ 'channel_username': channel_username,
247
+ 'telegram_message_id': message_id,
248
+ 'telegram_url': telegram_url or f"https://t.me/{channel_username.replace('@', '')}/{message_id}",
249
+ 'youtube_video_id': youtube_id,
250
+ 'processed_at': firestore.SERVER_TIMESTAMP,
251
+ 'status': 'completed' if youtube_id else 'failed'
252
+ }
253
+ doc_ref = self.firestore_db.collection(self.collection_name).document(doc_id)
254
+ doc_ref.set(doc_data)
255
+ web_logger.add_log('INFO', f"Marked video {message_id} as processed in Firebase")
256
+ except Exception as e:
257
+ web_logger.add_log('ERROR', f"Error marking video as processed: {e}")
258
+
259
+ def get_processed_videos_count(self):
260
+ """Get count of processed videos from Firebase"""
261
+ try:
262
+ collection_ref = self.firestore_db.collection(self.collection_name)
263
+ docs = collection_ref.where('status', '==', 'completed').stream()
264
+ count = sum(1 for _ in docs)
265
+ return count
266
+ except Exception as e:
267
+ web_logger.add_log('ERROR', f"Error getting processed videos count: {e}")
268
+ return 0
269
+
270
+ async def get_channel_videos(self, limit=10, offset=0):
271
+ """Get videos from Telegram channel with offset support"""
272
+ # Use user config or fall back to env var
273
+ channel_username = self.user_config.get('TELEGRAM_CHANNEL_USERNAME') or os.environ.get('TELEGRAM_CHANNEL_USERNAME')
274
+ if not channel_username:
275
+ web_logger.add_log('ERROR', "TELEGRAM_CHANNEL_USERNAME is not set in user config or environment variables.")
276
+ # Return empty result or raise an error
277
+ return {
278
+ 'videos': [],
279
+ 'processed_count': 0,
280
+ 'total_checked': 0,
281
+ 'last_message_id': None
282
+ }
283
+ try:
284
+ entity = await self.telegram_client.get_entity(channel_username)
285
+ videos = []
286
+ processed_count = 0
287
+ total_checked = 0
288
+ last_message_id = None
289
+ async for message in self.telegram_client.iter_messages(entity, limit=limit, offset_id=offset):
290
+ total_checked += 1
291
+ last_message_id = message.id
292
+ # Check if message has video
293
+ if message.video and message.video.mime_type.startswith('video/'):
294
+ telegram_url = f"https://t.me/{channel_username.replace('@', '')}/{message.id}"
295
+ if self.is_video_processed(channel_username, message.id):
296
+ processed_count += 1
297
+ web_logger.add_log('INFO', f"Skipping already processed video: {message.id}")
298
+ continue
299
+ else:
300
+ videos.append({
301
+ 'id': message.id,
302
+ 'message': message,
303
+ 'video': message.video,
304
+ 'caption': message.text or '',
305
+ 'date': message.date,
306
+ 'telegram_url': telegram_url,
307
+ 'channel_username': channel_username
308
+ })
309
+ web_logger.add_log('INFO', f"Found {len(videos)} new videos, {processed_count} already processed")
310
+ return {
311
+ 'videos': videos,
312
+ 'processed_count': processed_count,
313
+ 'total_checked': total_checked,
314
+ 'last_message_id': last_message_id
315
+ }
316
+ except Exception as e:
317
+ web_logger.add_log('ERROR', f"Error getting channel videos: {e}")
318
+ return {
319
+ 'videos': [],
320
+ 'processed_count': 0,
321
+ 'total_checked': 0,
322
+ 'last_message_id': None
323
+ }
324
+
325
+ async def download_video(self, video_info):
326
+ """Download video from Telegram"""
327
+ try:
328
+ message = video_info['message']
329
+ video_id = video_info['id']
330
+ filename = f"video_{video_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4"
331
+ filepath = self.download_dir / filename
332
+ web_logger.add_log('INFO', f"Downloading video {video_id}...")
333
+ await message.download_media(file=str(filepath))
334
+ web_logger.add_log('INFO', f"Downloaded: {filepath}")
335
+ return str(filepath)
336
+ except Exception as e:
337
+ web_logger.add_log('ERROR', f"Error downloading video {video_info['id']}: {e}")
338
+ return None
339
+
340
+ def upload_to_youtube(self, video_path, video_info):
341
+ """Upload video to YouTube"""
342
+ try:
343
+ # Use user config or fall back to env vars
344
+ title_prefix = self.user_config.get('YOUTUBE_TITLE_PREFIX', os.environ.get('YOUTUBE_TITLE_PREFIX', ''))
345
+ description_template = self.user_config.get('YOUTUBE_DESCRIPTION_TEMPLATE', os.environ.get('YOUTUBE_DESCRIPTION_TEMPLATE', 'Video from Telegram channel'))
346
+ tags_str = self.user_config.get('YOUTUBE_TAGS', os.environ.get('YOUTUBE_TAGS', 'telegram,video'))
347
+ category_id = self.user_config.get('YOUTUBE_CATEGORY_ID', os.environ.get('YOUTUBE_CATEGORY_ID', '22'))
348
+ privacy_status = self.user_config.get('YOUTUBE_PRIVACY_STATUS', os.environ.get('YOUTUBE_PRIVACY_STATUS', 'private'))
349
+
350
+ # Process tags
351
+ tags = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
352
 
353
+ # Prepare video metadata
354
+ title = f"{title_prefix}{video_info['caption'][:100]}"
355
+ if not title.strip():
356
+ title = f"Video from Telegram - {video_info['date'].strftime('%Y-%m-%d')}"
357
+ description = f"{description_template}\n"
358
+ description += f"Original Telegram post: {video_info['telegram_url']}\n"
359
+ description += f"Caption: {video_info['caption']}"
360
+ body = {
361
+ 'snippet': {
362
+ 'title': title,
363
+ 'description': description,
364
+ 'tags': tags,
365
+ 'categoryId': category_id
366
+ },
367
+ 'status': {
368
+ 'privacyStatus': privacy_status
369
+ }
370
+ }
371
+ # Upload video
372
+ media = MediaFileUpload(video_path, chunksize=-1, resumable=True)
373
+ request = self.youtube_service.videos().insert(
374
+ part=','.join(body.keys()),
375
+ body=body,
376
+ media_body=media
377
+ )
378
+ web_logger.add_log('INFO', f"Uploading {os.path.basename(video_path)} to YouTube...")
379
+ response = None
380
+ error = None
381
+ retry = 0
382
+ while response is None:
383
+ try:
384
+ status, response = request.next_chunk()
385
+ if status:
386
+ progress = int(status.progress() * 100)
387
+ web_logger.add_log('INFO', f"Upload progress: {progress}%")
388
+ except HttpError as e:
389
+ if e.resp.status in [500, 502, 503, 504]:
390
+ error = f"A retriable HTTP error {e.resp.status} occurred"
391
+ retry += 1
392
+ if retry > 3:
393
+ break
394
+ else:
395
+ raise
396
+ if response is not None:
397
+ video_id = response['id']
398
+ web_logger.add_log('INFO', f"Video uploaded successfully! YouTube ID: {video_id}")
399
+ return video_id
400
+ else:
401
+ web_logger.add_log('ERROR', f"Upload failed: {error}")
402
+ return None
403
+ except Exception as e:
404
+ web_logger.add_log('ERROR', f"Error uploading to YouTube: {e}")
405
+ return None
406
+
407
+ def cleanup_video(self, video_path):
408
+ """Delete downloaded video file"""
409
+ try:
410
+ os.remove(video_path)
411
+ web_logger.add_log('INFO', f"Cleaned up: {video_path}")
412
+ except Exception as e:
413
+ web_logger.add_log('ERROR', f"Error cleaning up {video_path}: {e}")
414
+
415
+ # Authentication decorator
416
  def login_required(f):
417
  @wraps(f)
418
  def decorated_function(*args, **kwargs):
419
+ if 'user_authenticated' not in session:
420
  return redirect(url_for('login'))
421
  return f(*args, **kwargs)
422
  return decorated_function
423
 
424
+ # Routes
425
+ @app.route('/')
426
+ def index():
427
+ if 'user_authenticated' not in session:
428
+ return redirect(url_for('login'))
429
+ return redirect(url_for('dashboard'))
430
 
431
  @app.route('/login', methods=['GET', 'POST'])
432
  def login():
433
  if request.method == 'POST':
434
+ username = request.form['username']
435
+ password = request.form['password']
436
+ # Check credentials from environment variables
437
  app_username = os.environ.get('APP_USERNAME')
438
  app_password = os.environ.get('APP_PASSWORD')
439
+ if username == app_username and password == app_password:
440
+ session['user_authenticated'] = True
441
+ flash('Login successful!', 'success')
442
+ return redirect(url_for('dashboard'))
 
 
443
  else:
444
+ flash('Invalid username or password!', 'error')
445
  return render_template('login.html')
446
 
447
+ @app.route('/logout')
448
+ def logout():
449
+ session.clear()
450
+ flash('Logged out successfully!', 'success')
451
+ return redirect(url_for('login'))
452
+
453
+ # --- Modify the dashboard route to pass user config ---
454
+ @app.route('/dashboard')
455
  @login_required
456
+ def dashboard():
457
+ global workflow_instance
458
+ if not workflow_instance:
459
+ workflow_instance = TelegramYouTubeWorkflow()
 
 
 
 
460
 
461
+ status = processing_status.copy()
462
+
463
+ # Get processed videos count
464
+ try:
465
+ status['total_processed'] = workflow_instance.get_processed_videos_count()
466
+ except:
467
+ status['total_processed'] = 0
468
+
469
+ # Load current user config to display in the form
470
+ current_config = load_user_config()
471
+
472
+ return render_template('dashboard.html', status=status, config=current_config)
473
+
474
+ # --- Add the new route to save configuration ---
475
+ @app.route('/save_config', methods=['POST'])
476
  @login_required
477
+ def save_config():
478
+ try:
479
+ # Get form data
480
+ config_data = {
481
+ 'TELEGRAM_CHANNEL_USERNAME': request.form.get('telegram_channel_username', '').strip(),
482
+ 'YOUTUBE_TITLE_PREFIX': request.form.get('youtube_title_prefix', '').strip(),
483
+ 'YOUTUBE_DESCRIPTION_TEMPLATE': request.form.get('youtube_description_template', '').strip(),
484
+ 'YOUTUBE_TAGS': request.form.get('youtube_tags', '').strip(), # Will be processed on load/save
485
+ 'YOUTUBE_CATEGORY_ID': request.form.get('youtube_category_id', '22').strip(),
486
+ 'YOUTUBE_PRIVACY_STATUS': request.form.get('youtube_privacy_status', 'private').strip().lower(),
487
+ }
488
 
489
+ # Basic validation (optional but good)
490
+ if config_data['TELEGRAM_CHANNEL_USERNAME'] and not config_data['TELEGRAM_CHANNEL_USERNAME'].startswith('@'):
491
+ return jsonify({'success': False, 'message': 'Telegram channel username must start with @'}), 400
492
 
493
+ if config_data['YOUTUBE_PRIVACY_STATUS'] not in ['public', 'private', 'unlisted']:
494
+ return jsonify({'success': False, 'message': 'Invalid YouTube privacy status. Use public, private, or unlisted.'}), 400
 
495
 
496
+ if not config_data['YOUTUBE_CATEGORY_ID'].isdigit():
497
+ return jsonify({'success': False, 'message': 'YouTube category ID must be a number.'}), 400
 
 
498
 
499
+ save_user_config(config_data)
500
+ # Optionally, update the workflow instance's config if it exists
501
+ global workflow_instance
502
+ if workflow_instance:
503
+ workflow_instance.user_config = config_data
504
+ return jsonify({'success': True, 'message': 'Configuration saved successfully!'})
505
+ except Exception as e:
506
+ web_logger.add_log('ERROR', f"Error saving config: {e}")
507
+ return jsonify({'success': False, 'message': f'Error saving configuration: {str(e)}'}), 500
508
+
509
+ @app.route('/youtube_auth')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  @login_required
511
  def youtube_auth():
512
+ global workflow_instance
513
+ if not workflow_instance:
514
+ workflow_instance = TelegramYouTubeWorkflow()
515
+ # Check if already authenticated
516
+ if workflow_instance.setup_youtube_client():
517
+ flash('YouTube is already authenticated!', 'success')
518
+ return redirect(url_for('dashboard'))
519
+ try:
520
+ auth_url = workflow_instance.get_youtube_auth_url()
521
+ return render_template('youtube_auth.html', auth_url=auth_url)
522
+ except Exception as e:
523
+ flash(f'Error generating auth URL: {str(e)}', 'error')
524
+ return redirect(url_for('dashboard'))
525
 
526
+ @app.route('/youtube_callback', methods=['POST'])
527
  @login_required
528
+ def youtube_callback():
529
+ global workflow_instance
530
+ auth_code = request.form.get('auth_code')
531
+ if not auth_code:
532
+ flash('Authorization code is required!', 'error')
533
+ return redirect(url_for('youtube_auth'))
534
+ if workflow_instance.complete_youtube_auth(auth_code):
535
+ flash('YouTube authentication successful!', 'success')
536
+ else:
537
+ flash('YouTube authentication failed!', 'error')
538
+ return redirect(url_for('dashboard'))
539
+
540
+ @app.route('/telegram_auth')
541
  @login_required
542
+ def telegram_auth():
543
+ return render_template('telegram_auth.html')
544
+
545
+ # --- Add routes for Telegram auth via web ---
546
+ # Global variables for Telegram auth
547
+ temp_telegram_client = None
548
+ temp_phone_number = None # Store phone number for sign_in
549
+
550
+ @app.route('/initiate_telegram_auth', methods=['POST'])
 
 
 
 
551
  @login_required
552
+ def initiate_telegram_auth():
553
+ """Initiates the Telegram authentication flow, potentially requesting OTP"""
554
+ global workflow_instance, temp_telegram_client, temp_phone_number
555
+ if not workflow_instance:
556
+ workflow_instance = TelegramYouTubeWorkflow()
557
+
558
+ try:
559
+ # Create a temporary client instance for auth
560
+ api_id = os.environ.get('TELEGRAM_API_ID')
561
+ api_hash = os.environ.get('TELEGRAM_API_HASH')
562
+ phone_number = os.environ.get('TELEGRAM_PHONE_NUMBER')
563
+
564
+ if not all([api_id, api_hash, phone_number]):
565
+ flash('Missing Telegram credentials in environment variables', 'error')
566
+ return redirect(url_for('telegram_auth'))
567
+
568
+ temp_phone_number = phone_number # Store for later use in callback
569
+ temp_telegram_client = TelegramClient('session', int(api_id), api_hash)
570
+
571
+ # Run the connection attempt asynchronously
572
+ asyncio.run(_attempt_telegram_connection(temp_telegram_client, phone_number))
573
+
574
+ # If we reach here without exception, auth might be complete or code needed
575
+ # Check if OTP is needed by seeing if the session file was created or flag is set
576
+ if os.path.exists('session.session'):
577
+ # Session file exists, likely authenticated
578
+ flash('Telegram connected successfully!', 'success')
579
+ # Clean up temp client
580
+ temp_telegram_client = None
581
+ temp_phone_number = None
582
+ session.pop('telegram_needs_code', None) # Ensure flag is cleared
583
+ return redirect(url_for('dashboard'))
584
+ else:
585
+ # If session file doesn't exist, OTP is likely needed
586
+ # The _attempt_telegram_connection should have set the flag if code is sent
587
+ # Redirect to the auth page which will show the OTP form
588
+ return redirect(url_for('telegram_auth'))
589
+
590
+ except Exception as e:
591
+ # Clean up on error
592
+ temp_telegram_client = None
593
+ temp_phone_number = None
594
+ session.pop('telegram_needs_code', None)
595
+ flash(f'Error initiating Telegram connection: {str(e)}', 'error')
596
+ web_logger.add_log('ERROR', f"Error initiating Telegram connection: {e}")
597
+ return redirect(url_for('telegram_auth'))
598
+
599
+ async def _attempt_telegram_connection(client, phone_number):
600
+ """Helper to attempt connection and signal if OTP is needed"""
601
+ try:
602
+ await client.connect()
603
+ if not await client.is_user_authorized():
604
+ # Send code request
605
+ await client.send_code_request(phone_number)
606
+ # Signal that OTP is needed
607
+ session['telegram_needs_code'] = True
608
+ web_logger.add_log('INFO', "Telegram code sent. Waiting for OTP input via web.")
609
+ # If already authorized, session file exists, connection is good
610
+ except errors.SessionPasswordNeededError:
611
+ # This means 2FA is enabled. The initial code request likely succeeded.
612
+ # We primarily rely on the 'code' callback for the initial OTP.
613
+ # Signal that OTP is needed (or handle password separately if needed).
614
+ session['telegram_needs_code'] = True
615
+ web_logger.add_log('INFO', "Telegram 2FA detected or code needed. Waiting for OTP input via web.")
616
+ except Exception as e:
617
+ web_logger.add_log('ERROR', f"Error during Telegram connection attempt: {e}")
618
+ raise e # Re-raise to be caught by initiate_telegram_auth
619
+
620
+ @app.route('/telegram_callback', methods=['POST'])
621
+ @login_required
622
+ def telegram_callback():
623
+ """Handles the submission of the OTP code"""
624
+ global temp_telegram_client, temp_phone_number
625
+ otp_code = request.form.get('otp_code')
626
+
627
+ if not otp_code:
628
+ flash('OTP code is required!', 'error')
629
+ session['telegram_needs_code'] = True # Ensure OTP form is shown
630
+ return redirect(url_for('telegram_auth'))
631
+
632
+ if not temp_telegram_client or not temp_phone_number:
633
+ flash('Telegram authentication session expired. Please restart the process.', 'error')
634
+ session.pop('telegram_needs_code', None)
635
+ return redirect(url_for('telegram_auth'))
636
+
637
+ try:
638
+ # Sign in with the provided code using the temporary client
639
+ asyncio.run(temp_telegram_client.sign_in(temp_phone_number, otp_code))
640
+ web_logger.add_log('INFO', "Telegram sign-in successful with provided OTP.")
641
+ flash('Telegram authentication successful!', 'success')
642
+
643
+ # Clean up
644
+ temp_telegram_client = None
645
+ temp_phone_number = None
646
+ session.pop('telegram_needs_code', None)
647
+
648
+ return redirect(url_for('dashboard'))
649
+
650
+ except errors.SessionPasswordNeededError:
651
+ # OTP was correct, but 2FA password is needed
652
+ # This requires a more complex flow. For now, we'll simplify.
653
+ # A better implementation would prompt for the 2FA password on the web page.
654
+ # For this version, we'll assume OTP handles initial auth for simplicity,
655
+ # or inform the user that 2FA might require a restart if the session isn't fully established.
656
+ # Let's assume the sign_in with code was enough for basic auth if no error is raised after.
657
+ # If sign_in succeeds, the session should be valid.
658
+ # However, SessionPasswordNeededError implies sign_in didn't complete fully.
659
+ # A robust solution would involve another input step for the password.
660
+ # For now, let's clear the OTP flag and redirect, assuming the code was enough or user needs to retry.
661
+ flash('Sign-in might require a 2FA password. If connection fails, please retry Telegram auth.', 'warning')
662
+ web_logger.add_log('WARNING', "Telegram 2FA password potentially needed after OTP (OTP submitted). Check if connection is successful on dashboard.")
663
+ # Clean up
664
+ temp_telegram_client = None
665
+ temp_phone_number = None
666
+ session.pop('telegram_needs_code', None)
667
+ # Redirect to dashboard to let the user check status or retry
668
+ return redirect(url_for('dashboard'))
669
+ except Exception as e:
670
+ # Clean up on error
671
+ temp_telegram_client = None
672
+ temp_phone_number = None
673
+ session.pop('telegram_needs_code', None)
674
+ flash(f'Telegram authentication failed: {str(e)}', 'error')
675
+ web_logger.add_log('ERROR', f"Error signing in to Telegram with OTP: {e}")
676
+ return redirect(url_for('telegram_auth'))
677
+
678
+ @app.route('/start_processing', methods=['POST'])
679
+ @login_required
680
+ def start_processing():
681
+ global processing_status, workflow_instance
682
+ if processing_status['is_running']:
683
+ return jsonify({'success': False, 'message': 'Processing is already running'})
684
  limit = int(request.form.get('limit', 5))
685
+ # Start processing in background thread
686
+ def run_processing():
687
+ asyncio.run(process_videos_background(limit))
688
+ thread = threading.Thread(target=run_processing)
689
+ thread.daemon = True
690
+ thread.start()
691
+ return jsonify({'success': True, 'message': 'Processing started'})
692
 
693
+ @app.route('/batch_confirmation', methods=['POST'])
694
+ @login_required
695
+ def batch_confirmation():
696
+ global processing_status
697
+ action = request.form.get('action') # 'continue' or 'stop'
698
+ if not processing_status['waiting_for_confirmation']:
699
+ return jsonify({'success': False, 'message': 'No confirmation pending'})
700
+ processing_status['user_decision'] = action
701
+ processing_status['waiting_for_confirmation'] = False
702
+ return jsonify({'success': True, 'message': f'Decision recorded: {action}'})
703
+
704
+ @app.route('/status')
705
+ @login_required
706
+ def get_status():
707
+ global processing_status
708
+ status = processing_status.copy()
709
+ status['logs'] = web_logger.get_logs()[-20:] # Last 20 logs
710
+ return jsonify(status)
711
+
712
+ @app.route('/clear_logs', methods=['POST'])
713
+ @login_required
714
+ def clear_logs():
715
+ web_logger.clear_logs()
716
+ return jsonify({'success': True})
717
+
718
+ async def process_videos_background(limit=5):
719
+ """Background processing function"""
720
+ global processing_status, workflow_instance
721
+ processing_status['is_running'] = True
722
+ processing_status['current_batch'] = 0
723
+ processing_status['processed_count'] = 0
724
+ processing_status['failed_count'] = 0
725
+ try:
726
+ # Setup clients
727
+ await workflow_instance.setup_telegram_client()
728
+ if not workflow_instance.setup_youtube_client():
729
+ web_logger.add_log('ERROR', 'YouTube not authenticated. Please authenticate first.')
730
+ return
731
+ batch_number = 1
732
+ offset = 0
733
+ while processing_status['is_running']:
734
+ processing_status['current_batch'] = batch_number
735
+ web_logger.add_log('INFO', f"Processing batch {batch_number} (limit: {limit})...")
736
+ # Get videos from Telegram
737
+ result = await workflow_instance.get_channel_videos(limit, offset)
738
+ videos = result['videos']
739
+ videos_already_processed = result['processed_count']
740
+ total_checked = result['total_checked']
741
+ last_message_id = result['last_message_id']
742
+ if not videos and videos_already_processed == 0:
743
+ web_logger.add_log('INFO', "No more videos found in the channel")
744
+ break
745
+ # Check if all videos were already processed
746
+ if not videos and videos_already_processed > 0:
747
+ # Ask user for confirmation
748
+ processing_status['waiting_for_confirmation'] = True
749
+ processing_status['confirmation_message'] = f"All {videos_already_processed} videos in batch {batch_number} have been processed previously. Continue to next batch?"
750
+ processing_status['user_decision'] = None
751
+ # Wait for user decision
752
+ while processing_status['waiting_for_confirmation'] and processing_status['is_running']:
753
+ await asyncio.sleep(1)
754
+ if processing_status.get('user_decision') != 'continue':
755
+ web_logger.add_log('INFO', "User chose not to continue. Stopping workflow.")
756
+ break
757
+ batch_number += 1
758
+ offset = last_message_id
759
+ continue
760
+ # Process videos in current batch
761
+ if videos:
762
+ for video_info in videos:
763
+ if not processing_status['is_running']:
764
+ break
765
+ try:
766
+ web_logger.add_log('INFO', f"Processing video {video_info['id']}...")
767
+ # Download video
768
+ video_path = await workflow_instance.download_video(video_info)
769
+ if not video_path:
770
+ processing_status['failed_count'] += 1
771
+ continue
772
+ # Upload to YouTube
773
+ youtube_id = workflow_instance.upload_to_youtube(video_path, video_info)
774
+ if youtube_id:
775
+ workflow_instance.mark_video_processed(
776
+ video_info['channel_username'],
777
+ video_info['id'],
778
+ youtube_id,
779
+ video_info['telegram_url']
780
+ )
781
+ processing_status['processed_count'] += 1
782
+ web_logger.add_log('INFO', f"Successfully processed video {video_info['id']} -> {youtube_id}")
783
+ else:
784
+ workflow_instance.mark_video_processed(
785
+ video_info['channel_username'],
786
+ video_info['id'],
787
+ None,
788
+ video_info['telegram_url']
789
+ )
790
+ processing_status['failed_count'] += 1
791
+ # Cleanup
792
+ workflow_instance.cleanup_video(video_path)
793
+ # Small delay
794
+ await asyncio.sleep(2)
795
+ except Exception as e:
796
+ web_logger.add_log('ERROR', f"Error processing video {video_info['id']}: {e}")
797
+ processing_status['failed_count'] += 1
798
+ continue
799
+ # Check if we should continue to next batch
800
+ if total_checked < limit:
801
+ web_logger.add_log('INFO', "Reached end of channel messages")
802
+ break
803
+ # Ask user for next batch confirmation
804
+ processing_status['waiting_for_confirmation'] = True
805
+ processing_status['confirmation_message'] = f"Batch {batch_number} completed. Process next {limit} videos?"
806
+ processing_status['user_decision'] = None
807
+ # Wait for user decision
808
+ while processing_status['waiting_for_confirmation'] and processing_status['is_running']:
809
+ await asyncio.sleep(1)
810
+ if processing_status.get('user_decision') != 'continue':
811
+ web_logger.add_log('INFO', "User chose not to continue. Stopping workflow.")
812
+ break
813
+ batch_number += 1
814
+ offset = last_message_id
815
+ web_logger.add_log('INFO', f"Workflow completed! Processed: {processing_status['processed_count']}, Failed: {processing_status['failed_count']}")
816
+ except Exception as e:
817
+ web_logger.add_log('ERROR', f"Workflow error: {e}")
818
+ finally:
819
+ processing_status['is_running'] = False
820
+ processing_status['waiting_for_confirmation'] = False
821
+ if workflow_instance.telegram_client:
822
+ await workflow_instance.telegram_client.disconnect()
823
+
824
+ @app.route('/stop_processing', methods=['POST'])
825
+ @login_required
826
+ def stop_processing():
827
+ global processing_status
828
+ processing_status['is_running'] = False
829
+ processing_status['waiting_for_confirmation'] = False
830
+ web_logger.add_log('INFO', "Processing stopped by user")
831
+ return jsonify({'success': True, 'message': 'Processing stopped'})
832
 
833
  if __name__ == '__main__':
834
+ port = int(os.environ.get('PORT', 5000))
835
+ app.run(host='0.0.0.0', port=port, debug=False)