setfunctionenvironment commited on
Commit
3eab1b1
·
verified ·
1 Parent(s): 017bb1c

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +374 -0
  2. requirements.txt +2 -0
app.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video Bulk Downloader - Single File Self-Hosted Solution
3
+ Run: pip install flask requests && python app.py
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import io
9
+ import zipfile
10
+ import requests
11
+ from flask import Flask, render_template_string, request, jsonify, send_file
12
+ from concurrent.futures import ThreadPoolExecutor
13
+ import threading
14
+ import time
15
+ import uuid
16
+
17
+ app = Flask(__name__)
18
+
19
+ # Track download progress
20
+ jobs = {}
21
+ jobs_lock = threading.Lock()
22
+
23
+ HTML_TEMPLATE = """
24
+ <!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>Video Downloader</title>
30
+ <style>
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body {
33
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
35
+ min-height: 100vh;
36
+ color: #fff;
37
+ padding: 40px 20px;
38
+ }
39
+ .container { max-width: 800px; margin: 0 auto; }
40
+ h1 {
41
+ text-align: center;
42
+ margin-bottom: 10px;
43
+ font-size: 2.5rem;
44
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+ .subtitle { text-align: center; color: #888; margin-bottom: 30px; }
50
+ .card {
51
+ background: rgba(255,255,255,0.05);
52
+ border-radius: 16px;
53
+ padding: 30px;
54
+ backdrop-filter: blur(10px);
55
+ border: 1px solid rgba(255,255,255,0.1);
56
+ }
57
+ textarea {
58
+ width: 100%;
59
+ height: 200px;
60
+ background: rgba(0,0,0,0.3);
61
+ border: 2px solid rgba(255,255,255,0.1);
62
+ border-radius: 12px;
63
+ color: #fff;
64
+ padding: 15px;
65
+ font-size: 14px;
66
+ resize: vertical;
67
+ transition: border-color 0.3s;
68
+ }
69
+ textarea:focus { outline: none; border-color: #ff6b6b; }
70
+ textarea::placeholder { color: #666; }
71
+ .btn {
72
+ width: 100%;
73
+ padding: 15px 30px;
74
+ margin-top: 20px;
75
+ border: none;
76
+ border-radius: 12px;
77
+ font-size: 16px;
78
+ font-weight: 600;
79
+ cursor: pointer;
80
+ transition: all 0.3s;
81
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
82
+ color: #1a1a2e;
83
+ }
84
+ .btn:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(255,107,107,0.3); }
85
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
86
+ #status {
87
+ margin-top: 25px;
88
+ padding: 20px;
89
+ background: rgba(0,0,0,0.2);
90
+ border-radius: 12px;
91
+ display: none;
92
+ }
93
+ .status-item {
94
+ padding: 10px;
95
+ margin: 5px 0;
96
+ border-radius: 8px;
97
+ font-size: 13px;
98
+ word-break: break-all;
99
+ }
100
+ .status-item.success { background: rgba(46,213,115,0.2); border-left: 3px solid #2ed573; }
101
+ .status-item.error { background: rgba(255,71,87,0.2); border-left: 3px solid #ff4757; }
102
+ .status-item.pending { background: rgba(255,255,255,0.1); border-left: 3px solid #feca57; }
103
+ .progress-bar {
104
+ height: 4px;
105
+ background: rgba(255,255,255,0.1);
106
+ border-radius: 2px;
107
+ margin-top: 15px;
108
+ overflow: hidden;
109
+ }
110
+ .progress-fill {
111
+ height: 100%;
112
+ background: linear-gradient(90deg, #ff6b6b, #feca57);
113
+ transition: width 0.3s;
114
+ width: 0%;
115
+ }
116
+ .stats {
117
+ display: flex;
118
+ justify-content: space-between;
119
+ margin-top: 10px;
120
+ font-size: 13px;
121
+ color: #888;
122
+ }
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div class="container">
127
+ <h1>Video Downloader</h1>
128
+ <p class="subtitle">Paste video links below, one per line</p>
129
+ <div class="card">
130
+ <textarea id="links" placeholder="https://example.com/video1
131
+ https://example.com/video2
132
+ https://example.com/video3"></textarea>
133
+ <button class="btn" id="downloadBtn" onclick="startDownload()">Download All</button>
134
+ <div id="status">
135
+ <div class="progress-bar"><div class="progress-fill" id="progress"></div></div>
136
+ <div class="stats">
137
+ <span id="statsText">0 / 0 completed</span>
138
+ <span id="statsPercent">0%</span>
139
+ </div>
140
+ <div id="statusList"></div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ <script>
145
+ let pollInterval;
146
+
147
+ async function startDownload() {
148
+ const links = document.getElementById('links').value.trim();
149
+ if (!links) return alert('Please enter at least one link');
150
+
151
+ const btn = document.getElementById('downloadBtn');
152
+ btn.disabled = true;
153
+ btn.textContent = 'Fetching videos...';
154
+
155
+ document.getElementById('status').style.display = 'block';
156
+ document.getElementById('statusList').innerHTML = '';
157
+ document.getElementById('progress').style.width = '0%';
158
+
159
+ try {
160
+ const resp = await fetch('/download', {
161
+ method: 'POST',
162
+ headers: {'Content-Type': 'application/json'},
163
+ body: JSON.stringify({links: links.split('\\n').filter(l => l.trim())})
164
+ });
165
+ const data = await resp.json();
166
+ pollInterval = setInterval(() => pollStatus(data.job_id), 500);
167
+ } catch(e) {
168
+ alert('Error: ' + e.message);
169
+ btn.disabled = false;
170
+ btn.textContent = 'Download All';
171
+ }
172
+ }
173
+
174
+ async function pollStatus(jobId) {
175
+ try {
176
+ const resp = await fetch('/status/' + jobId);
177
+ const data = await resp.json();
178
+
179
+ const list = document.getElementById('statusList');
180
+ list.innerHTML = '';
181
+
182
+ let completed = 0;
183
+ data.items.forEach(item => {
184
+ const div = document.createElement('div');
185
+ div.className = 'status-item ' + item.status;
186
+ div.textContent = item.url + ' - ' + item.message;
187
+ list.appendChild(div);
188
+ if (item.status !== 'pending') completed++;
189
+ });
190
+
191
+ const percent = Math.round((completed / data.items.length) * 100);
192
+ document.getElementById('progress').style.width = percent + '%';
193
+ document.getElementById('statsText').textContent = completed + ' / ' + data.items.length + ' completed';
194
+ document.getElementById('statsPercent').textContent = percent + '%';
195
+
196
+ if (data.complete) {
197
+ clearInterval(pollInterval);
198
+ const btn = document.getElementById('downloadBtn');
199
+
200
+ if (data.success_count > 0) {
201
+ btn.textContent = 'Starting download...';
202
+ window.location.href = '/get-zip/' + jobId;
203
+ setTimeout(() => {
204
+ btn.disabled = false;
205
+ btn.textContent = 'Download All';
206
+ }, 2000);
207
+ } else {
208
+ btn.disabled = false;
209
+ btn.textContent = 'Download All';
210
+ }
211
+ }
212
+ } catch(e) {
213
+ console.error(e);
214
+ }
215
+ }
216
+ </script>
217
+ </body>
218
+ </html>
219
+ """
220
+
221
+ def extract_id(url):
222
+ """Extract video ID from RedGifs URL"""
223
+ patterns = [
224
+ r'redgifs\.com/watch/([a-zA-Z]+)',
225
+ r'redgifs\.com/ifr/([a-zA-Z]+)',
226
+ r'^([a-zA-Z]+)$'
227
+ ]
228
+ for pattern in patterns:
229
+ match = re.search(pattern, url.strip())
230
+ if match:
231
+ return match.group(1).lower()
232
+ return None
233
+
234
+ def get_video_url(video_id):
235
+ """Get direct video URL from RedGifs API"""
236
+ headers = {
237
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
238
+ 'Accept': 'application/json',
239
+ }
240
+
241
+ token_resp = requests.get('https://api.redgifs.com/v2/auth/temporary', headers=headers)
242
+ token = token_resp.json().get('token')
243
+
244
+ if not token:
245
+ raise Exception("Failed to get auth token")
246
+
247
+ headers['Authorization'] = f'Bearer {token}'
248
+ api_url = f'https://api.redgifs.com/v2/gifs/{video_id}'
249
+ resp = requests.get(api_url, headers=headers)
250
+
251
+ if resp.status_code != 200:
252
+ raise Exception(f"API error: {resp.status_code}")
253
+
254
+ data = resp.json()
255
+ urls = data.get('gif', {}).get('urls', {})
256
+ return urls.get('hd') or urls.get('sd')
257
+
258
+ def download_video(url, job_id, index):
259
+ """Download a single video and store in memory"""
260
+ video_id = extract_id(url)
261
+
262
+ with jobs_lock:
263
+ jobs[job_id]['items'][index] = {
264
+ 'url': url,
265
+ 'status': 'pending',
266
+ 'message': 'Processing...'
267
+ }
268
+
269
+ if not video_id:
270
+ with jobs_lock:
271
+ jobs[job_id]['items'][index] = {
272
+ 'url': url,
273
+ 'status': 'error',
274
+ 'message': 'Invalid URL format'
275
+ }
276
+ return
277
+
278
+ try:
279
+ video_url = get_video_url(video_id)
280
+ if not video_url:
281
+ raise Exception("No video URL found")
282
+
283
+ resp = requests.get(video_url)
284
+ resp.raise_for_status()
285
+
286
+ with jobs_lock:
287
+ jobs[job_id]['videos'][video_id] = resp.content
288
+ jobs[job_id]['items'][index] = {
289
+ 'url': url,
290
+ 'status': 'success',
291
+ 'message': f'Ready: {video_id}.mp4'
292
+ }
293
+ except Exception as e:
294
+ with jobs_lock:
295
+ jobs[job_id]['items'][index] = {
296
+ 'url': url,
297
+ 'status': 'error',
298
+ 'message': str(e)
299
+ }
300
+
301
+ @app.route('/')
302
+ def index():
303
+ return render_template_string(HTML_TEMPLATE)
304
+
305
+ @app.route('/download', methods=['POST'])
306
+ def download():
307
+ data = request.json
308
+ links = data.get('links', [])
309
+
310
+ job_id = str(uuid.uuid4())
311
+
312
+ with jobs_lock:
313
+ jobs[job_id] = {
314
+ 'items': [{
315
+ 'url': link,
316
+ 'status': 'pending',
317
+ 'message': 'Queued...'
318
+ } for link in links],
319
+ 'videos': {},
320
+ 'complete': False
321
+ }
322
+
323
+ def run_downloads():
324
+ with ThreadPoolExecutor(max_workers=3) as executor:
325
+ futures = []
326
+ for i, link in enumerate(links):
327
+ futures.append(executor.submit(download_video, link, job_id, i))
328
+ for f in futures:
329
+ f.result()
330
+
331
+ with jobs_lock:
332
+ jobs[job_id]['complete'] = True
333
+
334
+ threading.Thread(target=run_downloads, daemon=True).start()
335
+ return jsonify({'job_id': job_id})
336
+
337
+ @app.route('/status/<job_id>')
338
+ def status(job_id):
339
+ with jobs_lock:
340
+ job = jobs.get(job_id, {'items': [], 'complete': True, 'videos': {}})
341
+ success_count = len(job.get('videos', {}))
342
+ return jsonify({
343
+ 'items': job['items'],
344
+ 'complete': job['complete'],
345
+ 'success_count': success_count
346
+ })
347
+
348
+ @app.route('/get-zip/<job_id>')
349
+ def get_zip(job_id):
350
+ with jobs_lock:
351
+ job = jobs.get(job_id)
352
+ if not job or not job['videos']:
353
+ return "No videos to download", 404
354
+
355
+ zip_buffer = io.BytesIO()
356
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
357
+ for video_id, content in job['videos'].items():
358
+ zf.writestr(f'{video_id}.mp4', content)
359
+
360
+ # Clean up job after download
361
+ del jobs[job_id]
362
+
363
+ zip_buffer.seek(0)
364
+ return send_file(
365
+ zip_buffer,
366
+ mimetype='application/zip',
367
+ as_attachment=True,
368
+ download_name='redgifs_download.zip'
369
+ )
370
+
371
+ if __name__ == '__main__':
372
+ port = int(os.environ.get('PORT', 7860))
373
+ print(f"\n Video Downloader running at http://localhost:{port}\n")
374
+ app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ requests