Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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,
|
| 6 |
-
from
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
from telethon
|
| 10 |
-
|
| 11 |
-
from
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
app = Flask(__name__)
|
| 15 |
-
app.secret_key = os.environ.get('FLASK_SECRET_KEY',
|
| 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 |
-
|
|
|
|
| 51 |
|
| 52 |
-
def
|
| 53 |
-
"""
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
logger.error(f"Failed to save session to Firestore: {e}")
|
| 63 |
|
| 64 |
-
def
|
| 65 |
-
"""
|
| 66 |
try:
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 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"
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def login_required(f):
|
| 80 |
@wraps(f)
|
| 81 |
def decorated_function(*args, **kwargs):
|
| 82 |
-
if '
|
| 83 |
return redirect(url_for('login'))
|
| 84 |
return f(*args, **kwargs)
|
| 85 |
return decorated_function
|
| 86 |
|
| 87 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
session['logged_in'] = True
|
| 99 |
-
return redirect(url_for('index'))
|
| 100 |
else:
|
| 101 |
-
flash('Invalid username or password
|
| 102 |
return render_template('login.html')
|
| 103 |
|
| 104 |
-
@app.route('/')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
@login_required
|
| 106 |
-
def
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
@login_required
|
| 117 |
-
def
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
|
| 124 |
-
if not
|
| 125 |
-
|
| 126 |
-
return render_template('telegram_auth.html', stage='credentials')
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
client = TelegramClient(StringSession(), int(api_id), api_hash)
|
| 131 |
-
loop = asyncio.get_event_loop()
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
@app.route('/
|
| 189 |
@login_required
|
| 190 |
-
def
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
'
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
@login_required
|
| 202 |
-
def
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 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
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
limit = int(request.form.get('limit', 5))
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
return jsonify(
|
| 227 |
|
| 228 |
-
@app.route('/
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
if __name__ == '__main__':
|
| 237 |
-
port = int(os.environ.get('PORT',
|
| 238 |
-
app.run(host='0.0.0.0', port=port, debug=
|
|
|
|
| 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)
|