Initial implementation of yt-dlp HLS proxy server

- Flask app with HLS proxy routes (/hls, /player, /)
- yt-dlp integration with 365-day in-memory cache
- URL validation with allowed domains (youtube, pornhub, etc)
- HTML5 HLS player with hls.js
- Unit tests: URL validation, cache, error handling
- Integration tests: ffmpeg-generated test video, full proxy chain
- Environment-based configuration (PORT, CACHE_TTL, LOG_LEVEL)
- MIT license
This commit is contained in:
Mikhail Yevchenko
2026-04-01 11:10:05 +00:00
parent 3d434dff6c
commit ff6e727ae7
13 changed files with 796 additions and 38 deletions
+71
View File
@@ -0,0 +1,71 @@
import logging
import os
import re
from urllib.parse import urlparse
logger = logging.getLogger(__name__)
ALLOWED_DOMAINS = os.getenv("ALLOWED_DOMAINS", "youtube.com,youtu.be,pornhub.com,xvideos.com,localhost,127.0.0.1").split(",")
VALIDATION_ENABLED = os.getenv("VALIDATION_ENABLED", "true").lower() == "true"
ALLOW_LOCAL = os.getenv("ALLOW_LOCAL", "true").lower() == "true"
def is_valid_url(url: str) -> bool:
if not VALIDATION_ENABLED:
return True
if not url:
return False
try:
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
return False
domain = parsed.netloc.lower()
if domain.startswith("www."):
domain = domain[4:]
if ALLOW_LOCAL and (domain in ("localhost", "127.0.0.1") or domain.startswith("localhost:") or domain.startswith("127.0.0.1:")):
return True
for allowed in ALLOWED_DOMAINS:
allowed = allowed.strip().lower()
if domain == allowed or domain.endswith(f".{allowed}"):
return True
return False
except Exception as e:
logger.error(f"URL validation error: {e}")
return False
def extract_video_id(url: str) -> str:
patterns = {
r'(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([a-zA-Z0-9_-]{11})': 'youtube',
r'pornhub\.com/view_video\.php\?viewkey=([a-zA-Z0-9]+)': 'pornhub',
}
for pattern, platform in patterns.items():
match = re.search(pattern, url)
if match:
return match.group(1)
return ""
def sanitize_path(path: str) -> str:
return path.replace("..", "").replace("//", "/").strip("/")
def get_error_message(status_code: int) -> str:
errors = {
400: "Bad Request - Invalid URL or parameters",
403: "Forbidden - Access denied",
404: "Not Found - Resource not found",
500: "Internal Server Error",
502: "Bad Gateway - Upstream error",
503: "Service Unavailable",
}
return errors.get(status_code, "Unknown error")