devusman commited on
Commit
a1a36a1
·
1 Parent(s): db540aa

feat: deployment

Browse files
Files changed (8) hide show
  1. .gitignore +7 -0
  2. Dockerfile +30 -0
  3. app.py +131 -0
  4. cookies.txt +9 -0
  5. requirements.txt +8 -0
  6. templates/index.html +500 -0
  7. test.py +54 -0
  8. test.txt +4 -0
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .mypy_cache
2
+ .venv
3
+
4
+ __pycache__/
5
+ *.pyc
6
+ env/
7
+ downloads/
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Install ffmpeg and other system dependencies
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ ffmpeg \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set the working directory in the container
10
+ WORKDIR /app
11
+
12
+ # Copy the dependencies file to the working directory
13
+ COPY requirements.txt .
14
+
15
+ # Install any needed packages specified in requirements.txt
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy the rest of the application code to the working directory
19
+ COPY . .
20
+
21
+ # Make port 7860 available to the world outside this container
22
+ # Hugging Face Spaces use 7860 by default
23
+ EXPOSE 7860
24
+
25
+ # Define environment variables
26
+ ENV FLASK_APP=app.py
27
+
28
+ # Run the app using gunicorn for production
29
+ # Use 0.0.0.0 to make it accessible from outside the container
30
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "app:app"]
app.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, Response, stream_with_context
2
+ import yt_dlp
3
+ import os
4
+ import shutil
5
+ import urllib.parse
6
+ import requests # We now use requests to stream the final content
7
+
8
+ app = Flask(__name__)
9
+
10
+ def get_ffmpeg_path():
11
+ """Finds the full path to the ffmpeg executable."""
12
+ return shutil.which('ffmpeg')
13
+
14
+ def sanitize_facebook_url(url):
15
+ """Cleans up Facebook URLs that are wrapped in a redirect link."""
16
+ try:
17
+ parsed_url = urllib.parse.urlparse(url)
18
+ if 'l.facebook.com' in parsed_url.netloc:
19
+ query_params = urllib.parse.parse_qs(parsed_url.query)
20
+ if 'u' in query_params:
21
+ clean_url = query_params['u'][0]
22
+ print(f"Sanitized URL: {url} -> {clean_url}", flush=True)
23
+ return clean_url
24
+ except Exception as e:
25
+ print(f"Could not sanitize URL, using original. Error: {e}", flush=True)
26
+ return url
27
+
28
+ @app.route('/')
29
+ def index():
30
+ """Serves the main HTML page."""
31
+ return render_template('index.html')
32
+
33
+ @app.route('/download', methods=['POST'])
34
+ def download():
35
+ """
36
+ This function now acts as a streaming proxy.
37
+ 1. Extracts the direct media URL using yt-dlp.
38
+ 2. Streams the content from that URL directly to the user's browser.
39
+ """
40
+ url = request.form.get('url')
41
+ output_format = request.form.get('format', 'mp4')
42
+
43
+ if not url:
44
+ return jsonify({'error': 'URL is required'}), 400
45
+
46
+ # --- Pre-flight checks ---
47
+ if not os.path.exists('cookies.txt'):
48
+ return jsonify({'error': '`cookies.txt` not found. Required for Facebook downloads.'}), 500
49
+
50
+ ffmpeg_path = get_ffmpeg_path()
51
+ if not ffmpeg_path:
52
+ return jsonify({'error': 'FFmpeg not found. It is required for format processing.'}), 500
53
+
54
+ try:
55
+ clean_url = sanitize_facebook_url(url)
56
+
57
+ # --- yt-dlp Options to EXTRACT INFO, NOT DOWNLOAD ---
58
+ ydl_opts = {
59
+ 'quiet': True, 'cookiefile': 'cookies.txt', 'noplaylist': True,
60
+ 'ffmpeg_location': os.path.dirname(ffmpeg_path)
61
+ }
62
+
63
+ # Determine format selection for yt-dlp
64
+ audio_formats = ['mp3', 'm4a', 'wav']
65
+ if output_format in audio_formats:
66
+ ydl_opts['format'] = 'bestaudio/best'
67
+ # We request a postprocessor but won't run it; this helps select the right stream
68
+ ydl_opts['postprocessors'] = [{'key': 'FFmpegExtractAudio', 'preferredcodec': output_format}]
69
+ else: # Video formats
70
+ ydl_opts['format'] = f'bestvideo[ext={output_format}]+bestaudio[ext=m4a]/best[ext={output_format}]/best'
71
+
72
+ # --- Stage 1: Get direct media URL from Facebook ---
73
+ print("Extracting media information from URL...")
74
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
75
+ info_dict = ydl.extract_info(clean_url, download=False)
76
+
77
+ # Find the best URL to stream from
78
+ stream_url = info_dict.get('url')
79
+ if not stream_url:
80
+ # Handle cases where video and audio are separate
81
+ requested_formats = info_dict.get('requested_formats')
82
+ if requested_formats:
83
+ # Typically, the first URL is the most relevant one (video or combined)
84
+ stream_url = requested_formats[0].get('url')
85
+ else:
86
+ return jsonify({'error': 'Could not extract a downloadable URL from the provided link.'}), 500
87
+
88
+ title = info_dict.get('title', 'facebook_content')
89
+ safe_title = "".join([c for c in title if c.isalpha() or c.isdigit() or c in (' ', '-', '_')]).rstrip()
90
+ download_name = f'{safe_title}.{output_format}'
91
+
92
+ # --- Stage 2: Stream the content from the direct URL to the user ---
93
+ print(f"Starting to stream from direct URL for: {download_name}")
94
+ # Make a HEAD request first to get the total size for the progress bar
95
+ head_req = requests.head(stream_url, allow_redirects=True, timeout=10)
96
+ total_size = int(head_req.headers.get('content-length', 0))
97
+
98
+ # Make the streaming GET request
99
+ stream_req = requests.get(stream_url, stream=True, allow_redirects=True, timeout=15)
100
+
101
+ # Check if the request was successful
102
+ if not stream_req.ok:
103
+ return jsonify({'error': f'Failed to fetch media. Status: {stream_req.status_code}'}), 500
104
+
105
+ def generate_content():
106
+ """A generator function that yields chunks of the download."""
107
+ for chunk in stream_req.iter_content(chunk_size=8192):
108
+ if chunk:
109
+ yield chunk
110
+
111
+ # Prepare and return the streaming response
112
+ mime_types = {'mp4': 'video/mp4', 'webm': 'video/webm', 'mp3': 'audio/mpeg', 'm4a': 'audio/mp4', 'wav': 'audio/wav'}
113
+ mimetype = mime_types.get(output_format, 'application/octet-stream')
114
+
115
+ response = Response(stream_with_context(generate_content()), mimetype=mimetype)
116
+ response.headers['Content-Disposition'] = f'attachment; filename="{download_name}"'
117
+ # Crucially, we provide the Content-Length for the progress bar
118
+ if total_size > 0:
119
+ response.headers['Content-Length'] = total_size
120
+
121
+ return response
122
+
123
+ except Exception as e:
124
+ error_message = str(e)
125
+ print(f"An unexpected error occurred: {error_message}")
126
+ if "private" in error_message.lower() or "login" in error_message.lower():
127
+ return jsonify({'error': 'This content is private or requires login. Please check `cookies.txt`.'}), 403
128
+ return jsonify({'error': 'An unknown error occurred. The link may be invalid or private.'}), 500
129
+
130
+ if __name__ == '__main__':
131
+ app.run(debug=True, host='0.0.0.0', port=7860)
cookies.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # This file is generated by yt-dlp. Do not edit.
3
+
4
+ .facebook.com TRUE / TRUE 1796890983 datr ZgkLaeUf04X_Z64d2osIDiRy
5
+ .facebook.com TRUE / TRUE 1770354288 fr 01mWyo4bom3O5LIia.AWcJdxDktq7qLh5y68sNC9JdRLm-fVK_hdm42oxpaa2b7zSn4UQ.BpCwlm..AAA.0.0.BpDs9x.AWf1imU5ilyIp07pbgfjE73blTQ
6
+ .facebook.com TRUE / TRUE 1762417538 presence C%7B%22t3%22%3A%5B%5D%2C%22utc3%22%3A1762331050912%2C%22v%22%3A1%7D
7
+ .facebook.com TRUE / TRUE 1796891043 sb ZgkLaXd4J6Y7zBpMdScBr7q4
8
+ .facebook.com TRUE / TRUE 1762935850 wd 1920x1065
9
+ .facebook.com TRUE / TRUE 0 noscript 1
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ yt-dlp
3
+ ffmpeg-python
4
+ requests
5
+ gunicorn
6
+ gallery-dl
7
+ beautifulsoup4
8
+ selenium
templates/index.html ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Facebook Video & Audio Downloader</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #1877f2;
10
+ --primary-hover: #166fe5;
11
+ --background-color: #f0f2f5;
12
+ --container-bg: #ffffff;
13
+ --text-color: #1c1e21;
14
+ --subtle-text: #606770;
15
+ --border-color: #dddfe2;
16
+ --error-bg: #fff0f0;
17
+ --error-text: #d8000c;
18
+ --progress-bg: #e4e6ea;
19
+ --progress-fill: var(--primary-color);
20
+ }
21
+ body {
22
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
23
+ Helvetica, Arial, sans-serif;
24
+ background-color: var(--background-color);
25
+ color: var(--text-color);
26
+ margin: 0;
27
+ padding: 20px;
28
+ display: flex;
29
+ justify-content: center;
30
+ align-items: flex-start;
31
+ min-height: 100vh;
32
+ }
33
+ .container {
34
+ background: var(--container-bg);
35
+ padding: 2rem;
36
+ border-radius: 8px;
37
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
38
+ width: 100%;
39
+ max-width: 600px;
40
+ transition: all 0.3s ease-in-out;
41
+ }
42
+ h1 {
43
+ text-align: center;
44
+ color: var(--primary-color);
45
+ margin-bottom: 1.5rem;
46
+ font-size: 1.8rem;
47
+ }
48
+ .tabs {
49
+ display: flex;
50
+ border-bottom: 1px solid var(--border-color);
51
+ margin-bottom: 1.5rem;
52
+ }
53
+ .tab-button {
54
+ background: none;
55
+ color: var(--subtle-text);
56
+ padding: 1rem;
57
+ border: none;
58
+ cursor: pointer;
59
+ font-size: 1rem;
60
+ font-weight: 600;
61
+ transition: color 0.2s, border-bottom 0.2s;
62
+ border-bottom: 3px solid transparent;
63
+ flex-grow: 1;
64
+ }
65
+ .tab-button.active {
66
+ color: var(--primary-color);
67
+ border-bottom-color: var(--primary-color);
68
+ }
69
+ .tab-content {
70
+ display: none;
71
+ }
72
+ .tab-content.active {
73
+ display: block;
74
+ }
75
+ form {
76
+ display: flex;
77
+ flex-direction: column;
78
+ }
79
+ input[type="text"],
80
+ select {
81
+ padding: 1rem;
82
+ margin-bottom: 1rem;
83
+ border: 1px solid var(--border-color);
84
+ border-radius: 6px;
85
+ font-size: 1rem;
86
+ width: 100%;
87
+ box-sizing: border-box;
88
+ }
89
+ button {
90
+ background-color: var(--primary-color);
91
+ color: white;
92
+ padding: 1rem;
93
+ border: none;
94
+ border-radius: 6px;
95
+ cursor: pointer;
96
+ font-size: 1.1rem;
97
+ font-weight: 600;
98
+ transition: background-color 0.3s;
99
+ }
100
+ button:hover:not(:disabled) {
101
+ background-color: var(--primary-hover);
102
+ }
103
+ button:disabled {
104
+ background-color: #a0bdf5;
105
+ cursor: not-allowed;
106
+ }
107
+ #status-container {
108
+ margin-top: 1.5rem;
109
+ text-align: center;
110
+ font-weight: 500;
111
+ }
112
+ #status.error {
113
+ color: var(--error-text);
114
+ background-color: var(--error-bg);
115
+ padding: 0.8rem;
116
+ border-radius: 6px;
117
+ }
118
+ .spinner {
119
+ border: 4px solid rgba(0, 0, 0, 0.1);
120
+ width: 36px;
121
+ height: 36px;
122
+ border-radius: 50%;
123
+ border-left-color: var(--primary-color);
124
+ animation: spin 1s linear infinite;
125
+ display: none;
126
+ margin: 20px auto;
127
+ }
128
+ @keyframes spin {
129
+ to {
130
+ transform: rotate(360deg);
131
+ }
132
+ }
133
+ .note {
134
+ font-size: 0.9rem;
135
+ color: var(--subtle-text);
136
+ margin-top: 1rem;
137
+ background-color: #f7f8fa;
138
+ padding: 0.8rem;
139
+ border-radius: 6px;
140
+ text-align: center;
141
+ }
142
+ .important-note {
143
+ font-size: 0.9rem;
144
+ color: #946c00;
145
+ margin-bottom: 1.5rem;
146
+ background-color: #fffbe6;
147
+ padding: 0.8rem;
148
+ border-radius: 6px;
149
+ text-align: center;
150
+ }
151
+ #progress-container {
152
+ display: none;
153
+ margin-top: 1rem;
154
+ text-align: center;
155
+ }
156
+ #progress-bar {
157
+ width: 100%;
158
+ height: 20px;
159
+ background-color: var(--progress-bg);
160
+ border-radius: 10px;
161
+ overflow: hidden;
162
+ display: block;
163
+ margin-bottom: 0.5rem;
164
+ }
165
+ #progress-bar::-webkit-progress-bar {
166
+ background-color: var(--progress-bg);
167
+ border-radius: 10px;
168
+ }
169
+ #progress-bar::-webkit-progress-value {
170
+ background-color: var(--progress-fill);
171
+ border-radius: 10px;
172
+ }
173
+ #progress-bar::-moz-progress-bar {
174
+ background-color: var(--progress-fill);
175
+ border-radius: 10px;
176
+ }
177
+ #progress-text {
178
+ font-weight: 600;
179
+ color: var(--subtle-text);
180
+ }
181
+ #control-buttons {
182
+ display: none;
183
+ margin-top: 1rem;
184
+ }
185
+ #pause-btn,
186
+ #cancel-btn {
187
+ margin: 0 0.5rem;
188
+ padding: 0.5rem 1rem;
189
+ font-size: 1rem;
190
+ }
191
+ #pause-btn.paused {
192
+ background-color: #28a745;
193
+ }
194
+ #pause-btn.paused:hover {
195
+ background-color: #218838;
196
+ }
197
+ </style>
198
+ </head>
199
+ <body>
200
+ <div class="container">
201
+ <h1>Facebook Video & Audio Downloader</h1>
202
+
203
+ <div class="tabs">
204
+ <button class="tab-button active" onclick="openTab(event, 'video')">
205
+ Video
206
+ </button>
207
+ <button class="tab-button" onclick="openTab(event, 'audio')">
208
+ Audio
209
+ </button>
210
+ </div>
211
+
212
+ <div id="video" class="tab-content active">
213
+ <form id="videoForm">
214
+ <input
215
+ type="text"
216
+ name="url"
217
+ placeholder="Enter Facebook Video URL"
218
+ required
219
+ />
220
+ <select name="format">
221
+ <option value="mp4">MP4</option>
222
+ <option value="webm">WebM</option>
223
+ </select>
224
+ <button type="submit">Download Video</button>
225
+ </form>
226
+ <p class="note">
227
+ Download Facebook videos in MP4 or WebM format. The download will
228
+ appear in your browser's download manager.
229
+ </p>
230
+ </div>
231
+
232
+ <div id="audio" class="tab-content">
233
+ <form id="audioForm">
234
+ <input
235
+ type="text"
236
+ name="url"
237
+ placeholder="Enter Facebook Video URL"
238
+ required
239
+ />
240
+ <select name="format">
241
+ <option value="mp3">MP3</option>
242
+ <option value="m4a">M4A</option>
243
+ <option value="wav">WAV</option>
244
+ </select>
245
+ <button type="submit">Download Audio</button>
246
+ </form>
247
+ <p class="note">
248
+ Extract audio from Facebook videos in various formats. The download
249
+ will appear in your browser's download manager.
250
+ </p>
251
+ </div>
252
+
253
+ <div id="status-container">
254
+ <div id="spinner" class="spinner"></div>
255
+ <div id="progress-container">
256
+ <progress id="progress-bar" value="0" max="100"></progress>
257
+ <div id="progress-text">0%</div>
258
+ </div>
259
+ <div id="control-buttons">
260
+ <button id="pause-btn">Pause</button>
261
+ <button id="cancel-btn" style="background-color: #dc3545">
262
+ Cancel
263
+ </button>
264
+ </div>
265
+ <div id="status"></div>
266
+ </div>
267
+ </div>
268
+
269
+ <script>
270
+ let isDownloading = false;
271
+ let paused = false;
272
+ let currentReader = null;
273
+ let currentChunks = [];
274
+ let currentReceived = 0;
275
+ let currentTotal = 0;
276
+ let currentResponse = null;
277
+
278
+ function openTab(evt, tabName) {
279
+ document
280
+ .querySelectorAll(".tab-content")
281
+ .forEach((tc) => tc.classList.remove("active"));
282
+ document
283
+ .querySelectorAll(".tab-button")
284
+ .forEach((tb) => tb.classList.remove("active"));
285
+ document.getElementById(tabName).classList.add("active");
286
+ evt.currentTarget.classList.add("active");
287
+ }
288
+
289
+ async function handleSubmit(e) {
290
+ e.preventDefault();
291
+
292
+ if (isDownloading) {
293
+ document.getElementById("status").textContent =
294
+ "A download is already in progress. Please wait or pause/cancel the current one.";
295
+ document.getElementById("status").classList.add("error");
296
+ return;
297
+ }
298
+
299
+ const form = e.target;
300
+ const submitButton = form.querySelector('button[type="submit"]');
301
+ const status = document.getElementById("status");
302
+ const spinner = document.getElementById("spinner");
303
+ const progressContainer = document.getElementById("progress-container");
304
+ const progressBar = document.getElementById("progress-bar");
305
+ const progressText = document.getElementById("progress-text");
306
+ const controlButtons = document.getElementById("control-buttons");
307
+
308
+ // Disable all download buttons
309
+ document
310
+ .querySelectorAll('button[type="submit"]')
311
+ .forEach((btn) => (btn.disabled = true));
312
+ isDownloading = true;
313
+ paused = false;
314
+
315
+ spinner.style.display = "block";
316
+ submitButton.disabled = true;
317
+ status.textContent =
318
+ "Initializing... Please wait, this may take a moment.";
319
+ status.classList.remove("error");
320
+ progressContainer.style.display = "none";
321
+ progressBar.value = 0;
322
+ progressText.textContent = "0%";
323
+ controlButtons.style.display = "none";
324
+
325
+ try {
326
+ const formData = new FormData(form);
327
+ const response = await fetch("/download", {
328
+ method: "POST",
329
+ body: formData,
330
+ });
331
+
332
+ if (!response.ok) {
333
+ const data = await response.json();
334
+ throw new Error(data.error || "An unknown server error occurred.");
335
+ }
336
+
337
+ status.textContent = "Streaming download...";
338
+ spinner.style.display = "none";
339
+ progressContainer.style.display = "block";
340
+ controlButtons.style.display = "block";
341
+
342
+ currentTotal = parseInt(
343
+ response.headers.get("Content-Length") || "0"
344
+ );
345
+ currentReceived = 0;
346
+ currentChunks = [];
347
+ currentReader = response.body.getReader();
348
+ currentResponse = response;
349
+
350
+ // Set up pause button
351
+ const pauseBtn = document.getElementById("pause-btn");
352
+ pauseBtn.onclick = togglePause;
353
+ pauseBtn.textContent = "Pause";
354
+ pauseBtn.classList.remove("paused");
355
+
356
+ // Set up cancel button
357
+ const cancelBtn = document.getElementById("cancel-btn");
358
+ cancelBtn.onclick = cancelDownload;
359
+
360
+ readChunk();
361
+ } catch (error) {
362
+ status.textContent = `Error: ${error.message}`;
363
+ status.classList.add("error");
364
+ resetDownloadState();
365
+ }
366
+ }
367
+
368
+ async function readChunk() {
369
+ if (!currentReader || !isDownloading) return;
370
+
371
+ try {
372
+ // If paused, wait and check again
373
+ while (paused && isDownloading) {
374
+ await new Promise((resolve) => setTimeout(resolve, 100));
375
+ }
376
+
377
+ if (!isDownloading) return;
378
+
379
+ const { done, value } = await currentReader.read();
380
+ if (done) {
381
+ // Download complete
382
+ await completeDownload();
383
+ return;
384
+ }
385
+
386
+ currentChunks.push(value);
387
+ currentReceived += value.length;
388
+
389
+ if (currentTotal > 0) {
390
+ const percent = Math.min(
391
+ (currentReceived / currentTotal) * 100,
392
+ 100
393
+ ).toFixed(2);
394
+ document.getElementById("progress-bar").value = percent;
395
+ document.getElementById(
396
+ "progress-text"
397
+ ).textContent = `${percent}%`;
398
+ } else {
399
+ document.getElementById("progress-text").textContent = `${(
400
+ currentReceived /
401
+ 1024 /
402
+ 1024
403
+ ).toFixed(1)} MB`;
404
+ }
405
+
406
+ if (isDownloading) {
407
+ readChunk();
408
+ }
409
+ } catch (err) {
410
+ if (isDownloading) {
411
+ throw err;
412
+ }
413
+ }
414
+ }
415
+
416
+ function togglePause() {
417
+ paused = !paused;
418
+ const pauseBtn = document.getElementById("pause-btn");
419
+ if (paused) {
420
+ pauseBtn.textContent = "Resume";
421
+ pauseBtn.classList.add("paused");
422
+ document.getElementById("status").textContent = "Download paused.";
423
+ } else {
424
+ pauseBtn.textContent = "Pause";
425
+ pauseBtn.classList.remove("paused");
426
+ document.getElementById("status").textContent =
427
+ "Streaming download...";
428
+ readChunk(); // Resume reading
429
+ }
430
+ }
431
+
432
+ async function cancelDownload() {
433
+ isDownloading = false;
434
+ paused = false;
435
+ if (currentReader) {
436
+ await currentReader.cancel();
437
+ }
438
+ document.getElementById("status").textContent = "Download cancelled.";
439
+ document.getElementById("status").classList.add("error");
440
+ resetDownloadState();
441
+ }
442
+
443
+ async function completeDownload() {
444
+ isDownloading = false;
445
+ paused = false;
446
+
447
+ const blob = new Blob(currentChunks);
448
+ let downloadName = "facebook_content";
449
+ const contentDisposition = currentResponse.headers.get(
450
+ "Content-Disposition"
451
+ );
452
+ if (contentDisposition && contentDisposition.includes("attachment")) {
453
+ const filenameRegex = /filename[^;=\n]*="?([^";\n]*)"?/;
454
+ const matches = filenameRegex.exec(contentDisposition);
455
+ if (matches != null && matches[1]) {
456
+ downloadName = decodeURIComponent(matches[1]);
457
+ }
458
+ }
459
+
460
+ const url = window.URL.createObjectURL(blob);
461
+ const a = document.createElement("a");
462
+ a.style.display = "none";
463
+ a.href = url;
464
+ a.download = downloadName;
465
+ document.body.appendChild(a);
466
+ a.click();
467
+ window.URL.revokeObjectURL(url);
468
+ a.remove();
469
+
470
+ document.getElementById("status").textContent =
471
+ "Download complete! Check your browser's downloads.";
472
+ document.getElementById("progress-container").style.display = "none";
473
+ document.getElementById("control-buttons").style.display = "none";
474
+ resetDownloadState();
475
+ }
476
+
477
+ function resetDownloadState() {
478
+ document
479
+ .querySelectorAll('button[type="submit"]')
480
+ .forEach((btn) => (btn.disabled = false));
481
+ document.getElementById("spinner").style.display = "none";
482
+ document.getElementById("progress-container").style.display = "none";
483
+ document.getElementById("control-buttons").style.display = "none";
484
+ isDownloading = false;
485
+ paused = false;
486
+ currentReader = null;
487
+ currentChunks = [];
488
+ currentReceived = 0;
489
+ currentResponse = null;
490
+ }
491
+
492
+ document
493
+ .getElementById("videoForm")
494
+ .addEventListener("submit", handleSubmit);
495
+ document
496
+ .getElementById("audioForm")
497
+ .addEventListener("submit", handleSubmit);
498
+ </script>
499
+ </body>
500
+ </html>
test.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+
4
+ def download_fb_content(url, cookies_path):
5
+ """
6
+ Downloads content from a direct Facebook URL using gallery-dl.
7
+
8
+ Args:
9
+ url (str): The direct URL to the Facebook content.
10
+ cookies_path (str): The path to the cookies.txt file.
11
+ """
12
+
13
+ # First, check if the cookies file exists before attempting to download.
14
+ if not os.path.exists(cookies_path):
15
+ print(f"Error: Cookies file not found at '{cookies_path}'")
16
+ print("Please ensure the file exists and the path is correct.")
17
+ return # Stop the function if cookies are missing
18
+
19
+ # --- Direct download logic ---
20
+ print("-" * 20)
21
+ print(f"Running gallery-dl for: {url}")
22
+ print("-" * 20)
23
+
24
+ cmd = [
25
+ "gallery-dl",
26
+ "--cookies", cookies_path,
27
+ url
28
+ ]
29
+
30
+ try:
31
+ # Run the command and check for any errors during execution.
32
+ subprocess.run(cmd, check=True)
33
+ print("\nDownload completed successfully.")
34
+
35
+ except FileNotFoundError:
36
+ print("\n--- ERROR ---")
37
+ print("Command 'gallery-dl' not found.")
38
+ print("Please make sure gallery-dl is installed and accessible in your system's PATH.")
39
+
40
+ except subprocess.CalledProcessError as e:
41
+ print("\n--- ERROR ---")
42
+ print(f"gallery-dl returned an error (Exit Code: {e.returncode}).")
43
+ print("This could be due to an invalid URL, private content, or expired cookies.")
44
+ print("Try running the command directly in your terminal for more detailed error messages.")
45
+
46
+ # --- EXAMPLE ---
47
+
48
+ # Set the path to your cookies file.
49
+ # Make sure it's in the same directory as this script or provide the full path.
50
+ cookies_file = "cookies.txt"
51
+
52
+ print("--- Downloading Facebook Story Content ---")
53
+ story_url = "https://www.facebook.com/stories/108639588358960/UzpfSVNDOjE1NjIwNDMzMjg0ODk0ODA=/?view_single=false"
54
+ download_fb_content(story_url, cookies_file)
test.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ https://www.facebook.com/stories/122760247048923/UzpfSVNDOjEwNTY3ODYxNDY0NzUyMTY=/?bucket_count=9&source=story_tray
3
+
4
+ https://www.facebook.com/share/v/1BqrcUihBf/