Phoe2004 commited on
Commit
e43c0bf
Β·
verified Β·
1 Parent(s): 21504de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -15
app.py CHANGED
@@ -1,6 +1,7 @@
1
- import os, re, uuid, threading, time, subprocess
2
  from pathlib import Path
3
- from flask import Flask, request, jsonify, send_from_directory, Response
 
4
 
5
  BASE_DIR = Path(__file__).parent
6
  COOKIES_FILE = str(BASE_DIR / 'm_youtube_com_cookies.txt')
@@ -8,24 +9,180 @@ OUTPUT_DIR = BASE_DIR / 'outputs'
8
  OUTPUT_DIR.mkdir(exist_ok=True)
9
 
10
  app = Flask(__name__)
 
11
 
12
- # Quality formats
 
 
 
 
 
 
13
  QUALITY_FMT = {
 
14
  '720': 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720]/best',
15
  '480': 'best[height<=480]/best',
16
  '360': 'best[height<=360]/best',
 
17
  }
18
 
 
 
 
 
 
 
 
 
 
19
  def ytdlp_download(out_path, url, quality='720'):
20
- fmt = QUALITY_FMT.get(quality, QUALITY_FMT['720'])
21
- cmd = ['yt-dlp', '-f', fmt, '--merge-output-format', 'mp4', '-o', out_path, url]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  if os.path.exists(COOKIES_FILE):
23
  cmd += ['--cookies', COOKIES_FILE]
24
- subprocess.run(cmd, check=True, timeout=300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @app.route('/')
27
  def index():
28
- return "YouTube Downloader & Auto Poster is running!"
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  @app.route('/api/download', methods=['POST'])
31
  def api_download():
@@ -34,19 +191,166 @@ def api_download():
34
  quality = data.get('quality', '720')
35
 
36
  if not url:
37
- return jsonify(ok=False, msg='No URL')
38
 
39
  tid = uuid.uuid4().hex[:8]
40
- out_path = str(OUTPUT_DIR / f'{tid}.mp4')
 
41
 
42
  def _bg():
43
- try:
44
- ytdlp_download(out_path, url, quality)
45
- except Exception as e:
46
- print(f"Error: {e}")
47
 
48
  threading.Thread(target=_bg).start()
49
- return jsonify(ok=True, tid=tid, msg='Download started')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  if __name__ == '__main__':
52
- app.run(host='0.0.0.0', port=7860)
 
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')
 
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():
 
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; }
246
+ hr { border-color: #333; }
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() {
303
+ const res = await fetch('/api/bot/start', {method: 'POST'});
304
+ const data = await res.json();
305
+ alert(data.msg);
306
+ refreshStatus();
307
+ }
308
+
309
+ async function stopBot() {
310
+ const res = await fetch('/api/bot/stop', {method: 'POST'});
311
+ const data = await res.json();
312
+ alert(data.msg);
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
+
342
+ function escapeHtml(text) {
343
+ const div = document.createElement('div');
344
+ div.textContent = text;
345
+ return div.innerHTML;
346
+ }
347
+
348
+ refreshStatus();
349
+ setInterval(refreshStatus, 5000);
350
+ </script>
351
+ </body>
352
+ </html>
353
+ '''
354
 
355
  if __name__ == '__main__':
356
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)