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