feat: deployment
Browse files- .gitignore +7 -0
- Dockerfile +30 -0
- app.py +131 -0
- cookies.txt +9 -0
- requirements.txt +8 -0
- templates/index.html +500 -0
- test.py +54 -0
- 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/
|