Phoe2004 commited on
Commit
7d802e8
Β·
verified Β·
1 Parent(s): e43c0bf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -223
app.py CHANGED
@@ -1,245 +1,216 @@
1
- import os, re, uuid, threading, time, subprocess, json
2
  from pathlib import Path
3
  from datetime import datetime
4
- from flask import Flask, request, jsonify, send_from_directory, Response, render_template_string
5
 
6
  BASE_DIR = Path(__file__).parent
7
  COOKIES_FILE = str(BASE_DIR / 'm_youtube_com_cookies.txt')
 
8
  OUTPUT_DIR = BASE_DIR / 'outputs'
9
  OUTPUT_DIR.mkdir(exist_ok=True)
10
 
11
  app = Flask(__name__)
12
- app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
13
 
14
- # ── Global variables for bot control ──
15
  bot_thread = None
16
  bot_running = False
17
  bot_logs = []
18
  log_lock = threading.Lock()
19
 
20
- # ── Quality formats ──
21
- QUALITY_FMT = {
22
- '1080': 'bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080]/best',
23
- '720': 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]/best',
24
- '480': 'best[height<=480]/best',
25
- '360': 'best[height<=360]/best',
26
- 'audio': 'bestaudio[ext=m4a]/bestaudio',
27
- }
28
-
29
  def add_log(msg):
30
- """Add timestamped message to debug log"""
31
  with log_lock:
32
  timestamp = datetime.now().strftime('%H:%M:%S')
33
  bot_logs.append(f'[{timestamp}] {msg}')
34
  if len(bot_logs) > 200:
35
  bot_logs.pop(0)
36
- print(f'[LOG] {msg}')
 
 
 
 
 
 
 
 
 
 
37
 
38
- def ytdlp_download(out_path, url, quality='720'):
39
- """Download video using yt-dlp"""
40
- add_log(f'Downloading: {url[:80]}...')
 
 
41
 
42
- if quality == 'audio':
43
- fmt = 'bestaudio[ext=m4a]/bestaudio'
44
- cmd = ['yt-dlp', '-f', fmt, '-x', '--audio-format', 'mp3', '-o', out_path, url]
45
- else:
46
- fmt = QUALITY_FMT.get(quality, QUALITY_FMT['720'])
47
- cmd = ['yt-dlp', '-f', fmt, '--merge-output-format', 'mp4', '-o', out_path, url]
48
 
49
- if os.path.exists(COOKIES_FILE):
50
- cmd += ['--cookies', COOKIES_FILE]
51
- add_log('Using YouTube cookies')
52
 
53
- add_log(f'Running: {" ".join(cmd[:4])}...')
54
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
55
-
56
- if result.returncode == 0:
57
- add_log(f'βœ… Download complete: {out_path}')
58
- return True
59
- else:
60
- add_log(f'❌ Download failed: {result.stderr[:200]}')
 
 
 
 
 
61
  return False
62
 
63
- def get_video_info(url):
64
- """Get video metadata without downloading"""
65
- add_log(f'Fetching info: {url[:80]}...')
66
- cmd = ['yt-dlp', '--dump-json', '--no-playlist', '--no-warnings']
67
- if os.path.exists(COOKIES_FILE):
68
- cmd += ['--cookies', COOKIES_FILE]
69
- cmd.append(url)
70
-
71
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
72
- if result.returncode != 0:
73
- raise Exception(result.stderr[:200])
74
-
75
- data = json.loads(result.stdout)
76
- return {
77
- 'title': data.get('title', 'Unknown'),
78
- 'thumbnail': data.get('thumbnail', ''),
79
- 'duration': data.get('duration', 0),
80
- 'uploader': data.get('uploader', ''),
81
- 'view_count': data.get('view_count', 0),
82
- }
83
-
84
- # ── Auto Poster Bot Functions ──
85
- def auto_poster_worker():
86
- """Background worker for YouTube channel monitoring"""
87
- global bot_running
88
- add_log('πŸ€– Auto Poster Bot Started')
89
- add_log(f'Monitoring channel: UCzHMxST9eZUo0XIaSyaTizA')
90
-
91
- last_video_id = None
92
- check_count = 0
93
 
94
- while bot_running:
95
- check_count += 1
96
- add_log(f'πŸ” Checking RSS feed... (#{check_count})')
97
-
98
- try:
99
- import feedparser
100
- rss_url = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCzHMxST9eZUo0XIaSyaTizA'
101
- feed = feedparser.parse(rss_url)
102
-
103
- if feed.entries:
104
- latest = feed.entries[0]
105
- video_id = latest.get('yt_videoid', '')
106
- title = latest.get('title', 'No Title')
107
-
108
- if last_video_id is None:
109
- last_video_id = video_id
110
- add_log(f'πŸ“Œ Initialized with last video: {video_id} - {title[:50]}')
111
- elif video_id != last_video_id:
112
- add_log(f'πŸ“Ή NEW VIDEO DETECTED! ID: {video_id}')
113
- add_log(f'πŸ“Ή Title: {title}')
114
- add_log(f'πŸ“Ή URL: https://youtube.com/watch?v={video_id}')
115
-
116
- # Download and process
117
- video_path = OUTPUT_DIR / f'{video_id}.mp4'
118
- if ytdlp_download(str(video_path), f'https://youtube.com/watch?v={video_id}', '720'):
119
- add_log(f'βœ… Downloaded: {title[:50]}')
120
- # TODO: Add TikTok upload here
121
- add_log(f'πŸ“€ Would upload to TikTok here...')
122
-
123
- last_video_id = video_id
124
- else:
125
- add_log(f'πŸ“­ No new videos (last: {last_video_id[:8]}...)')
126
- else:
127
- add_log('⚠️ No entries found in RSS feed')
128
-
129
- except Exception as e:
130
- add_log(f'❌ RSS error: {str(e)[:100]}')
131
-
132
- # Wait 5 minutes
133
- for _ in range(300):
134
- if not bot_running:
135
- break
136
- time.sleep(1)
137
 
138
- add_log('πŸ›‘ Auto Poster Bot Stopped')
139
-
140
- def start_bot():
141
- """Start the auto poster bot"""
142
- global bot_thread, bot_running
143
- if bot_running:
144
- return {'ok': False, 'msg': 'Bot is already running'}
145
 
146
- bot_running = True
147
- bot_thread = threading.Thread(target=auto_poster_worker, daemon=True)
148
- bot_thread.start()
149
- add_log('▢️ Bot started by user')
150
- return {'ok': True, 'msg': 'Bot started'}
151
-
152
- def stop_bot():
153
- """Stop the auto poster bot"""
154
- global bot_running
155
- if not bot_running:
156
- return {'ok': False, 'msg': 'Bot is not running'}
157
 
158
- bot_running = False
159
- add_log('⏹️ Bot stopped by user')
160
- return {'ok': True, 'msg': 'Bot stopped'}
161
-
162
- def get_bot_status():
163
- """Get current bot status"""
164
- return {
165
- 'running': bot_running,
166
- 'logs': bot_logs[-50:], # Last 50 logs
167
- 'log_count': len(bot_logs)
168
- }
169
 
170
  # ── Routes ──
171
  @app.route('/')
172
  def index():
173
  return render_template_string(HTML_TEMPLATE)
174
 
175
- @app.route('/api/info', methods=['POST'])
176
- def api_info():
 
177
  data = request.get_json() or {}
178
- url = data.get('url', '').strip()
179
- if not url:
 
 
 
180
  return jsonify({'ok': False, 'msg': 'No URL provided'})
 
 
 
 
 
 
 
 
 
 
 
 
181
  try:
182
- info = get_video_info(url)
183
- return jsonify({'ok': True, **info})
184
- except Exception as e:
185
- return jsonify({'ok': False, 'msg': str(e)})
 
186
 
187
- @app.route('/api/download', methods=['POST'])
188
- def api_download():
189
- data = request.get_json() or {}
190
- url = data.get('url', '').strip()
191
- quality = data.get('quality', '720')
192
 
193
- if not url:
194
- return jsonify({'ok': False, 'msg': 'No URL provided'})
195
 
196
- tid = uuid.uuid4().hex[:8]
197
- ext = 'mp3' if quality == 'audio' else 'mp4'
198
- out_path = str(OUTPUT_DIR / f'dl_{tid}.{ext}')
199
 
200
- def _bg():
201
- ytdlp_download(out_path, url, quality)
202
 
203
- threading.Thread(target=_bg).start()
204
- return jsonify({'ok': True, 'tid': tid, 'msg': 'Download started'})
 
 
 
 
 
205
 
206
  @app.route('/api/bot/start', methods=['POST'])
207
  def api_bot_start():
208
- return jsonify(start_bot())
 
 
 
 
 
 
 
209
 
210
  @app.route('/api/bot/stop', methods=['POST'])
211
  def api_bot_stop():
212
- return jsonify(stop_bot())
 
 
 
213
 
214
  @app.route('/api/bot/status', methods=['GET'])
215
  def api_bot_status():
216
- return jsonify(get_bot_status())
217
 
218
- @app.route('/api/bot/logs', methods=['GET'])
219
- def api_bot_logs():
220
- limit = request.args.get('limit', 50, type=int)
221
- with log_lock:
222
- return jsonify({'ok': True, 'logs': bot_logs[-limit:], 'total': len(bot_logs)})
223
-
224
- @app.route('/outputs/<path:fname>')
225
- def serve_output(fname):
226
- return send_from_directory(str(OUTPUT_DIR), fname)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- # ── HTML Template ──
229
  HTML_TEMPLATE = '''
230
  <!DOCTYPE html>
231
  <html>
232
  <head>
233
- <title>YouTube Downloader + Auto Poster</title>
234
  <style>
235
  body { font-family: monospace; max-width: 800px; margin: 0 auto; padding: 20px; background: #0a0a0a; color: #0f0; }
236
  .card { background: #111; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 20px; }
237
  button { background: #00aa00; color: black; border: none; padding: 8px 16px; cursor: pointer; margin-right: 8px; }
238
- button.stop { background: #aa0000; }
239
- button.danger { background: #aa0000; }
240
- input, select { padding: 8px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #222; color: #0f0; border: 1px solid #333; }
241
- pre { background: #1a1a1a; padding: 10px; overflow-x: auto; font-size: 12px; border-radius: 4px; }
242
- .log-box { height: 300px; overflow-y: scroll; background: #0a0a0a; border: 1px solid #333; padding: 10px; font-size: 11px; }
243
  .status { padding: 8px; border-radius: 4px; margin-bottom: 10px; }
244
  .status.running { background: #1a3a1a; color: #0f0; }
245
  .status.stopped { background: #3a1a1a; color: #f00; }
@@ -247,56 +218,55 @@ HTML_TEMPLATE = '''
247
  </style>
248
  </head>
249
  <body>
250
- <h1>🎬 YouTube Downloader + Auto Poster Bot</h1>
251
 
252
- <!-- Bot Control -->
253
  <div class="card">
254
  <h2>πŸ€– Auto Poster Bot</h2>
255
- <div id="bot-status" class="status stopped">πŸ”΄ Bot is STOPPED</div>
256
- <button id="start-btn" onclick="startBot()">▢️ START BOT</button>
257
- <button id="stop-btn" class="stop" onclick="stopBot()">⏹️ STOP BOT</button>
258
- <button onclick="refreshStatus()">πŸ”„ Refresh Status</button>
259
  <hr>
260
- <h3>πŸ“œ Debug Log (Last 50 lines)</h3>
261
- <div id="log-box" class="log-box"></div>
262
- </div>
263
-
264
- <!-- Video Info -->
265
- <div class="card">
266
- <h2>πŸ“Ή Video Info</h2>
267
- <input type="text" id="info-url" placeholder="YouTube URL">
268
- <button onclick="getInfo()">Get Info</button>
269
- <pre id="info-result"></pre>
270
  </div>
271
 
272
- <!-- Download -->
273
  <div class="card">
274
- <h2>⬇️ Download Video</h2>
275
- <input type="text" id="dl-url" placeholder="YouTube URL">
276
- <select id="quality">
 
277
  <option value="1080">1080p</option>
278
  <option value="720" selected>720p</option>
279
  <option value="480">480p</option>
280
- <option value="360">360p</option>
281
- <option value="audio">MP3 Audio</option>
282
  </select>
283
- <button onclick="startDownload()">Download</button>
284
- <pre id="dl-result"></pre>
 
 
 
 
 
 
 
 
 
285
  </div>
286
 
287
  <script>
288
  async function refreshStatus() {
289
  const res = await fetch('/api/bot/status');
290
  const data = await res.json();
291
- const statusDiv = document.getElementById('bot-status');
292
  if (data.running) {
293
  statusDiv.className = 'status running';
294
- statusDiv.innerHTML = '🟒 Bot is RUNNING';
295
  } else {
296
  statusDiv.className = 'status stopped';
297
- statusDiv.innerHTML = 'πŸ”΄ Bot is STOPPED';
298
  }
299
- document.getElementById('log-box').innerHTML = data.logs.map(l => `<div>${escapeHtml(l)}</div>`).join('');
300
  }
301
 
302
  async function startBot() {
@@ -313,29 +283,36 @@ HTML_TEMPLATE = '''
313
  refreshStatus();
314
  }
315
 
316
- async function getInfo() {
317
- const url = document.getElementById('info-url').value;
318
- if (!url) { alert('Enter URL'); return; }
319
- const res = await fetch('/api/info', {
 
 
 
 
320
  method: 'POST',
321
  headers: {'Content-Type': 'application/json'},
322
- body: JSON.stringify({url})
323
  });
324
  const data = await res.json();
325
- document.getElementById('info-result').innerHTML = JSON.stringify(data, null, 2);
 
326
  }
327
 
328
- async function startDownload() {
329
- const url = document.getElementById('dl-url').value;
330
- const quality = document.getElementById('quality').value;
331
- if (!url) { alert('Enter URL'); return; }
332
- const res = await fetch('/api/download', {
333
- method: 'POST',
334
- headers: {'Content-Type': 'application/json'},
335
- body: JSON.stringify({url, quality})
336
- });
 
 
337
  const data = await res.json();
338
- document.getElementById('dl-result').innerHTML = JSON.stringify(data, null, 2);
339
  refreshStatus();
340
  }
341
 
@@ -353,4 +330,4 @@ HTML_TEMPLATE = '''
353
  '''
354
 
355
  if __name__ == '__main__':
356
- app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
 
1
+ import os, uuid, threading, time, subprocess, json, requests
2
  from pathlib import Path
3
  from datetime import datetime
4
+ from flask import Flask, request, jsonify, send_from_directory, render_template_string
5
 
6
  BASE_DIR = Path(__file__).parent
7
  COOKIES_FILE = str(BASE_DIR / 'm_youtube_com_cookies.txt')
8
+ TIKTOK_COOKIE_FILE = str(BASE_DIR / 'cookies.txt')
9
  OUTPUT_DIR = BASE_DIR / 'outputs'
10
  OUTPUT_DIR.mkdir(exist_ok=True)
11
 
12
  app = Flask(__name__)
 
13
 
14
+ # ── Global variables ──
15
  bot_thread = None
16
  bot_running = False
17
  bot_logs = []
18
  log_lock = threading.Lock()
19
 
 
 
 
 
 
 
 
 
 
20
  def add_log(msg):
 
21
  with log_lock:
22
  timestamp = datetime.now().strftime('%H:%M:%S')
23
  bot_logs.append(f'[{timestamp}] {msg}')
24
  if len(bot_logs) > 200:
25
  bot_logs.pop(0)
26
+ print(msg)
27
+
28
+ def get_tiktok_sessionid():
29
+ try:
30
+ content = open(TIKTOK_COOKIE_FILE, 'r').read()
31
+ for part in content.replace('\n', ';').split(';'):
32
+ if part.strip().startswith('sessionid='):
33
+ return part.split('=', 1)[1]
34
+ except:
35
+ return None
36
+ return None
37
 
38
+ def upload_to_tiktok(video_path, title):
39
+ sessionid = get_tiktok_sessionid()
40
+ if not sessionid:
41
+ add_log('❌ TikTok sessionid not found')
42
+ return False
43
 
44
+ add_log(f'πŸ“€ Uploading: {title[:50]}...')
 
 
 
 
 
45
 
46
+ url = 'https://www.tiktok.com/api/v1/video/upload/'
47
+ headers = {'User-Agent': 'Mozilla/5.0', 'Cookie': f'sessionid={sessionid}'}
48
+ caption = f"{title}\n#movierecap #myanmar #fyp #viral"
49
 
50
+ try:
51
+ with open(video_path, 'rb') as f:
52
+ files = {'video': ('video.mp4', f, 'video/mp4')}
53
+ data = {'caption': caption}
54
+ response = requests.post(url, headers=headers, data=data, files=files, timeout=120)
55
+ if response.status_code == 200 and response.json().get('status_code') == 0:
56
+ add_log('βœ… Upload success!')
57
+ return True
58
+ else:
59
+ add_log(f'❌ Upload failed: {response.text[:100]}')
60
+ return False
61
+ except Exception as e:
62
+ add_log(f'❌ Error: {e}')
63
  return False
64
 
65
+ def download_video(url, quality='720'):
66
+ tid = uuid.uuid4().hex[:8]
67
+ out_path = OUTPUT_DIR / f'{tid}.mp4'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ fmt_map = {'1080': 'best[height<=1080]', '720': 'best[height<=720]', '480': 'best[height<=480]'}
70
+ fmt = fmt_map.get(quality, 'best[height<=720]')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ cmd = ['yt-dlp', '-f', fmt, '--merge-output-format', 'mp4', '-o', str(out_path), url]
73
+ if os.path.exists(COOKIES_FILE):
74
+ cmd += ['--cookies', COOKIES_FILE]
 
 
 
 
75
 
76
+ add_log(f'πŸ“₯ Downloading: {url[:60]}...')
77
+ result = subprocess.run(cmd, capture_output=True, text=True)
 
 
 
 
 
 
 
 
 
78
 
79
+ if result.returncode == 0 and out_path.exists():
80
+ add_log(f'βœ… Downloaded: {out_path.name}')
81
+ return str(out_path)
82
+ else:
83
+ add_log(f'❌ Download failed: {result.stderr[:100]}')
84
+ return None
 
 
 
 
 
85
 
86
  # ── Routes ──
87
  @app.route('/')
88
  def index():
89
  return render_template_string(HTML_TEMPLATE)
90
 
91
+ @app.route('/api/upload/manual', methods=['POST'])
92
+ def api_upload_manual():
93
+ """Manual upload - provide video URL and title"""
94
  data = request.get_json() or {}
95
+ video_url = data.get('url', '').strip()
96
+ title = data.get('title', '').strip()
97
+ quality = data.get('quality', '720')
98
+
99
+ if not video_url:
100
  return jsonify({'ok': False, 'msg': 'No URL provided'})
101
+ if not title:
102
+ return jsonify({'ok': False, 'msg': 'No title provided'})
103
+
104
+ # Download first
105
+ video_path = download_video(video_url, quality)
106
+ if not video_path:
107
+ return jsonify({'ok': False, 'msg': 'Download failed'})
108
+
109
+ # Then upload to TikTok
110
+ success = upload_to_tiktok(video_path, title)
111
+
112
+ # Cleanup
113
  try:
114
+ os.remove(video_path)
115
+ except:
116
+ pass
117
+
118
+ return jsonify({'ok': success, 'msg': 'Upload completed' if success else 'Upload failed'})
119
 
120
+ @app.route('/api/upload/file', methods=['POST'])
121
+ def api_upload_file():
122
+ """Manual upload - directly upload video file"""
123
+ if 'video' not in request.files:
124
+ return jsonify({'ok': False, 'msg': 'No video file'})
125
 
126
+ video = request.files['video']
127
+ title = request.form.get('title', 'Video')
128
 
129
+ # Save temp file
130
+ temp_path = OUTPUT_DIR / f'temp_{uuid.uuid4().hex[:8]}.mp4'
131
+ video.save(str(temp_path))
132
 
133
+ # Upload to TikTok
134
+ success = upload_to_tiktok(str(temp_path), title)
135
 
136
+ # Cleanup
137
+ try:
138
+ temp_path.unlink()
139
+ except:
140
+ pass
141
+
142
+ return jsonify({'ok': success, 'msg': 'Upload completed' if success else 'Upload failed'})
143
 
144
  @app.route('/api/bot/start', methods=['POST'])
145
  def api_bot_start():
146
+ global bot_thread, bot_running
147
+ if bot_running:
148
+ return jsonify({'ok': False, 'msg': 'Bot already running'})
149
+ bot_running = True
150
+ bot_thread = threading.Thread(target=auto_poster_worker, daemon=True)
151
+ bot_thread.start()
152
+ add_log('▢️ Bot started')
153
+ return jsonify({'ok': True, 'msg': 'Bot started'})
154
 
155
  @app.route('/api/bot/stop', methods=['POST'])
156
  def api_bot_stop():
157
+ global bot_running
158
+ bot_running = False
159
+ add_log('⏹️ Bot stopped')
160
+ return jsonify({'ok': True, 'msg': 'Bot stopped'})
161
 
162
  @app.route('/api/bot/status', methods=['GET'])
163
  def api_bot_status():
164
+ return jsonify({'running': bot_running, 'logs': bot_logs[-50:], 'tiktok_cookie': get_tiktok_sessionid() is not None})
165
 
166
+ def auto_poster_worker():
167
+ global bot_running
168
+ add_log('πŸ€– Auto Poster started')
169
+ CHANNEL_ID = 'UCUZHFZ9jIKrLroW8LcyJEQQ'
170
+ last_id = None
171
+
172
+ while bot_running:
173
+ try:
174
+ import feedparser
175
+ url = f'https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID}'
176
+ feed = feedparser.parse(url)
177
+
178
+ if feed.entries:
179
+ latest = feed.entries[0]
180
+ video_id = latest.get('yt_videoid', '')
181
+ title = latest.get('title', '')
182
+
183
+ if last_id is None:
184
+ last_id = video_id
185
+ add_log(f'πŸ“Œ Initialized: {video_id}')
186
+ elif video_id != last_id:
187
+ add_log(f'πŸ“Ή New video: {title[:50]}')
188
+ video_path = download_video(f'https://youtube.com/watch?v={video_id}', '720')
189
+ if video_path:
190
+ upload_to_tiktok(video_path, title)
191
+ try: os.remove(video_path)
192
+ except: pass
193
+ last_id = video_id
194
+ except Exception as e:
195
+ add_log(f'⚠️ Error: {e}')
196
+
197
+ for _ in range(300):
198
+ if not bot_running: break
199
+ time.sleep(1)
200
 
201
+ # ── HTML Template with Manual Upload ──
202
  HTML_TEMPLATE = '''
203
  <!DOCTYPE html>
204
  <html>
205
  <head>
206
+ <title>YouTube to TikTok Bot</title>
207
  <style>
208
  body { font-family: monospace; max-width: 800px; margin: 0 auto; padding: 20px; background: #0a0a0a; color: #0f0; }
209
  .card { background: #111; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 20px; }
210
  button { background: #00aa00; color: black; border: none; padding: 8px 16px; cursor: pointer; margin-right: 8px; }
211
+ button.red { background: #aa0000; }
212
+ input, select, textarea { padding: 8px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #222; color: #0f0; border: 1px solid #333; }
213
+ .log-box { height: 200px; overflow-y: scroll; background: #0a0a0a; border: 1px solid #333; padding: 10px; font-size: 11px; }
 
 
214
  .status { padding: 8px; border-radius: 4px; margin-bottom: 10px; }
215
  .status.running { background: #1a3a1a; color: #0f0; }
216
  .status.stopped { background: #3a1a1a; color: #f00; }
 
218
  </style>
219
  </head>
220
  <body>
221
+ <h1>🎬 YouTube to TikTok Bot</h1>
222
 
223
+ <!-- Auto Bot Control -->
224
  <div class="card">
225
  <h2>πŸ€– Auto Poster Bot</h2>
226
+ <div id="status" class="status stopped">πŸ”΄ STOPPED</div>
227
+ <button onclick="startBot()">▢️ START</button>
228
+ <button class="red" onclick="stopBot()">⏹️ STOP</button>
229
+ <button onclick="refreshStatus()">πŸ”„ Refresh</button>
230
  <hr>
231
+ <div id="log" class="log-box"></div>
 
 
 
 
 
 
 
 
 
232
  </div>
233
 
234
+ <!-- Manual Upload by URL -->
235
  <div class="card">
236
+ <h2>πŸ“€ Manual Upload (by URL)</h2>
237
+ <input type="text" id="manual-url" placeholder="YouTube URL">
238
+ <input type="text" id="manual-title" placeholder="Video Title">
239
+ <select id="manual-quality">
240
  <option value="1080">1080p</option>
241
  <option value="720" selected>720p</option>
242
  <option value="480">480p</option>
 
 
243
  </select>
244
+ <button onclick="manualUpload()">⬆️ Download & Upload</button>
245
+ <div id="manual-result"></div>
246
+ </div>
247
+
248
+ <!-- Manual Upload by File -->
249
+ <div class="card">
250
+ <h2>πŸ“ Manual Upload (by File)</h2>
251
+ <input type="file" id="file-input" accept="video/mp4">
252
+ <input type="text" id="file-title" placeholder="Video Title">
253
+ <button onclick="manualUploadFile()">⬆️ Upload File to TikTok</button>
254
+ <div id="file-result"></div>
255
  </div>
256
 
257
  <script>
258
  async function refreshStatus() {
259
  const res = await fetch('/api/bot/status');
260
  const data = await res.json();
261
+ const statusDiv = document.getElementById('status');
262
  if (data.running) {
263
  statusDiv.className = 'status running';
264
+ statusDiv.innerHTML = '🟒 RUNNING';
265
  } else {
266
  statusDiv.className = 'status stopped';
267
+ statusDiv.innerHTML = 'πŸ”΄ STOPPED';
268
  }
269
+ document.getElementById('log').innerHTML = data.logs.map(l => `<div>${escapeHtml(l)}</div>`).join('');
270
  }
271
 
272
  async function startBot() {
 
283
  refreshStatus();
284
  }
285
 
286
+ async function manualUpload() {
287
+ const url = document.getElementById('manual-url').value;
288
+ const title = document.getElementById('manual-title').value;
289
+ const quality = document.getElementById('manual-quality').value;
290
+ if (!url || !title) { alert('Enter URL and Title'); return; }
291
+
292
+ document.getElementById('manual-result').innerHTML = '⏳ Processing...';
293
+ const res = await fetch('/api/upload/manual', {
294
  method: 'POST',
295
  headers: {'Content-Type': 'application/json'},
296
+ body: JSON.stringify({url, title, quality})
297
  });
298
  const data = await res.json();
299
+ document.getElementById('manual-result').innerHTML = data.ok ? 'βœ… ' + data.msg : '❌ ' + data.msg;
300
+ refreshStatus();
301
  }
302
 
303
+ async function manualUploadFile() {
304
+ const file = document.getElementById('file-input').files[0];
305
+ const title = document.getElementById('file-title').value;
306
+ if (!file || !title) { alert('Select file and enter title'); return; }
307
+
308
+ const formData = new FormData();
309
+ formData.append('video', file);
310
+ formData.append('title', title);
311
+
312
+ document.getElementById('file-result').innerHTML = '⏳ Uploading...';
313
+ const res = await fetch('/api/upload/file', {method: 'POST', body: formData});
314
  const data = await res.json();
315
+ document.getElementById('file-result').innerHTML = data.ok ? 'βœ… ' + data.msg : '❌ ' + data.msg;
316
  refreshStatus();
317
  }
318
 
 
330
  '''
331
 
332
  if __name__ == '__main__':
333
+ app.run(host='0.0.0.0', port=7860, debug=False)