handle application/vnd.apple.mpegurl.
This commit is contained in:
186
backend/main.py
186
backend/main.py
@@ -6,6 +6,7 @@ from requests.adapters import HTTPAdapter
|
||||
from urllib3.util import Retry
|
||||
import yt_dlp
|
||||
import io
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Serve frontend static files under `/static` to avoid colliding with API routes
|
||||
app = Flask(__name__, static_folder='../frontend', static_url_path='/static')
|
||||
@@ -105,7 +106,7 @@ def videos_proxy():
|
||||
def index():
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
@app.route('/api/stream', methods=['POST', 'GET'])
|
||||
@app.route('/api/stream', methods=['POST', 'GET', 'HEAD'])
|
||||
def stream_video():
|
||||
# Note: <video> tags perform GET. To support your POST requirement,
|
||||
# we handle the URL via JSON post or URL params.
|
||||
@@ -118,62 +119,139 @@ def stream_video():
|
||||
if not video_url:
|
||||
return jsonify({"error": "No URL provided"}), 400
|
||||
|
||||
def generate():
|
||||
try:
|
||||
# Configure yt-dlp options
|
||||
ydl_opts = {
|
||||
'format': 'best[ext=mp4]/best[vcodec^=avc1]/best[vcodec^=vp]/best',
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'socket_timeout': 30,
|
||||
'retries': 3,
|
||||
'fragment_retries': 3,
|
||||
'http_headers': {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
},
|
||||
'skip_unavailable_fragments': True
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# Extract the info
|
||||
info = ydl.extract_info(video_url, download=False)
|
||||
|
||||
# Try to get the URL from the info dict (works for progressive downloads)
|
||||
stream_url = info.get('url')
|
||||
format_id = info.get('format_id')
|
||||
|
||||
# If no direct URL, try to get it from formats
|
||||
if not stream_url and 'formats' in info:
|
||||
# Find the best format that has a URL
|
||||
for fmt in info['formats']:
|
||||
if fmt.get('url'):
|
||||
stream_url = fmt.get('url')
|
||||
break
|
||||
|
||||
if not stream_url:
|
||||
yield b"Error: Could not extract stream URL"
|
||||
return
|
||||
|
||||
# Prepare headers for the stream request
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
|
||||
# Add any cookies or authentication headers from yt-dlp
|
||||
if 'http_headers' in info:
|
||||
headers.update(info['http_headers'])
|
||||
|
||||
# Stream the video from the extracted URL
|
||||
resp = session.get(stream_url, headers=headers, stream=True, timeout=30, allow_redirects=True)
|
||||
resp.raise_for_status()
|
||||
|
||||
def is_hls(url):
|
||||
return '.m3u8' in urllib.parse.urlparse(url).path
|
||||
|
||||
def is_direct_media(url):
|
||||
path = urllib.parse.urlparse(url).path.lower()
|
||||
return any(path.endswith(ext) for ext in ('.mp4', '.m4v', '.m4s', '.ts', '.webm', '.mov'))
|
||||
|
||||
def proxy_response(target_url, content_type_override=None):
|
||||
# Extract the base domain to spoof the referer
|
||||
parsed_uri = urllib.parse.urlparse(target_url)
|
||||
referer = f"{parsed_uri.scheme}://{parsed_uri.netloc}/"
|
||||
|
||||
safe_request_headers = {
|
||||
'User-Agent': request.headers.get('User-Agent'),
|
||||
'Referer': referer, # Vital for bypassing CDN blocks
|
||||
'Origin': referer
|
||||
}
|
||||
|
||||
# Pass through Range headers so the browser can 'sniff' the video
|
||||
if 'Range' in request.headers:
|
||||
safe_request_headers['Range'] = request.headers['Range']
|
||||
|
||||
resp = session.get(target_url, headers=safe_request_headers, stream=True, timeout=30, allow_redirects=True)
|
||||
|
||||
hop_by_hop = {
|
||||
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
|
||||
'te', 'trailers', 'transfer-encoding', 'upgrade'
|
||||
}
|
||||
|
||||
forwarded_headers = []
|
||||
for name, value in resp.headers.items():
|
||||
if name.lower() in hop_by_hop:
|
||||
continue
|
||||
if name.lower() == 'content-length':
|
||||
forwarded_headers.append((name, value))
|
||||
continue
|
||||
if name.lower() == 'content-type' and content_type_override:
|
||||
continue
|
||||
forwarded_headers.append((name, value))
|
||||
|
||||
if content_type_override:
|
||||
forwarded_headers.append(('Content-Type', content_type_override))
|
||||
|
||||
if request.method == 'HEAD':
|
||||
resp.close()
|
||||
return Response("", status=resp.status_code, headers=forwarded_headers)
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for chunk in resp.iter_content(chunk_size=1024 * 16):
|
||||
if chunk:
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
yield f"Error: {str(e)}".encode()
|
||||
finally:
|
||||
resp.close()
|
||||
|
||||
return Response(generate(), mimetype='video/mp4')
|
||||
return Response(generate(), status=resp.status_code, headers=forwarded_headers)
|
||||
|
||||
def proxy_hls_playlist(playlist_url):
|
||||
headers = {
|
||||
'User-Agent': request.headers.get('User-Agent', 'Mozilla/5.0'),
|
||||
'Accept': request.headers.get('Accept', '*/*')
|
||||
}
|
||||
resp = session.get(playlist_url, headers=headers, timeout=30)
|
||||
resp.raise_for_status()
|
||||
base_url = resp.url
|
||||
lines = resp.text.splitlines()
|
||||
rewritten = []
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
rewritten.append(line)
|
||||
continue
|
||||
absolute = urljoin(base_url, stripped)
|
||||
proxied = f"/api/stream?url={urllib.parse.quote(absolute, safe='')}"
|
||||
rewritten.append(proxied)
|
||||
if request.method == 'HEAD':
|
||||
return Response("", status=200, content_type='application/vnd.apple.mpegurl')
|
||||
|
||||
body = "\n".join(rewritten)
|
||||
return Response(body, status=200, content_type='application/vnd.apple.mpegurl')
|
||||
|
||||
if is_hls(video_url):
|
||||
try:
|
||||
return proxy_hls_playlist(video_url)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if is_direct_media(video_url):
|
||||
try:
|
||||
return proxy_response(video_url)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
try:
|
||||
# Configure yt-dlp options
|
||||
ydl_opts = {
|
||||
'format': 'best[ext=mp4]/best[vcodec^=avc1]/best[vcodec^=vp]/best',
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
'socket_timeout': 30,
|
||||
'retries': 3,
|
||||
'fragment_retries': 3,
|
||||
'http_headers': {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
},
|
||||
'skip_unavailable_fragments': True
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# Extract the info
|
||||
info = ydl.extract_info(video_url, download=False)
|
||||
|
||||
# Try to get the URL from the info dict (works for progressive downloads)
|
||||
stream_url = info.get('url')
|
||||
|
||||
# If no direct URL, try to get it from formats
|
||||
if not stream_url and 'formats' in info:
|
||||
# Find the best format that has a URL
|
||||
for fmt in info['formats']:
|
||||
if fmt.get('url'):
|
||||
stream_url = fmt.get('url')
|
||||
break
|
||||
|
||||
if not stream_url:
|
||||
return jsonify({"error": "Could not extract stream URL"}), 500
|
||||
|
||||
if is_hls(stream_url):
|
||||
return proxy_hls_playlist(stream_url)
|
||||
|
||||
return proxy_response(stream_url)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
# threaded=True allows multiple segments to be proxied at once
|
||||
app.run(host='0.0.0.0', port=5000, threaded=True)
|
||||
|
||||
Reference in New Issue
Block a user