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
+139
View File
@@ -0,0 +1,139 @@
import os
import subprocess
import time
import threading
import requests
import pytest
import sys
import urllib.parse
import http.server
import socketserver
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TEST_VIDEO_DIR = "/tmp/yt-dlp-test-video"
TEST_VIDEO_M3U8 = f"{TEST_VIDEO_DIR}/index.m3u8"
SERVER_PORT = 5002
TEST_HTTP_PORT = 8898
def generate_test_video():
os.makedirs(TEST_VIDEO_DIR, exist_ok=True)
cmd = [
"ffmpeg", "-y", "-f", "lavfi", "-i", "testsrc=duration=5:size=320x240:rate=24",
"-f", "lavfi", "-i", "sine=frequency=440:duration=5",
"-c:v", "libx264", "-c:a", "aac", "-strict", "experimental",
"-hls_time", "1", "-hls_list_size", "0",
"-hls_segment_filename", f"{TEST_VIDEO_DIR}/segment%03d.ts",
TEST_VIDEO_M3U8
]
subprocess.run(cmd, capture_output=True, timeout=60)
assert os.path.exists(TEST_VIDEO_M3U8), "HLS manifest not generated"
segments = [f for f in os.listdir(TEST_VIDEO_DIR) if f.endswith(".ts")]
assert len(segments) > 0, "No segments generated"
class QuietHTTPHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass
class ReusableTCPServer(socketserver.TCPServer):
allow_reuse_address = True
def serve_test_video():
os.chdir(TEST_VIDEO_DIR)
with ReusableTCPServer(("127.0.0.1", TEST_HTTP_PORT), QuietHTTPHandler) as httpd:
httpd.serve_forever()
def start_flask_app():
import app as flask_app
flask_app.app.run(host="127.0.0.1", port=SERVER_PORT, debug=False, use_reloader=False)
@pytest.fixture(scope="module")
def test_servers():
print("\nGenerating test video...")
generate_test_video()
print(f"Starting HTTP server for test video on port {TEST_HTTP_PORT}...")
http_thread = threading.Thread(target=serve_test_video, daemon=True)
http_thread.start()
time.sleep(1)
for _ in range(10):
try:
requests.get(f"http://127.0.0.1:{TEST_HTTP_PORT}/", timeout=1)
break
except:
time.sleep(0.5)
print("HTTP server ready")
print(f"Starting Flask proxy server on port {SERVER_PORT}...")
flask_thread = threading.Thread(target=start_flask_app, daemon=True)
flask_thread.start()
time.sleep(2)
print("Flask server ready")
yield
print("\nCleaning up...")
def test_direct_hls_access(test_servers):
"""Test that we can access the test HLS video directly"""
response = requests.get(f"http://127.0.0.1:{TEST_HTTP_PORT}/index.m3u8", timeout=5)
assert response.status_code == 200
assert "#EXTM3U" in response.text
print("Direct HLS access: OK")
def test_hls_playlist_proxy(test_servers):
"""Test proxying HLS playlist"""
video_url = f"http://127.0.0.1:{TEST_HTTP_PORT}/index.m3u8"
proxy_url = f"http://127.0.0.1:{SERVER_PORT}/hls?url={urllib.parse.quote(video_url, safe='')}"
response = requests.get(proxy_url, timeout=10)
assert response.status_code == 200
assert "#EXTM3U" in response.text
assert ".ts" in response.text
print("HLS playlist proxy: OK")
def test_hls_segment_proxy(test_servers):
"""Test proxying HLS segment"""
video_url = f"http://127.0.0.1:{TEST_HTTP_PORT}/index.m3u8"
proxy_url = f"http://127.0.0.1:{SERVER_PORT}/hls?url={urllib.parse.quote(video_url, safe='')}&path=segment000.ts"
response = requests.get(proxy_url, timeout=10)
assert response.status_code == 200
assert len(response.content) > 0
print("HLS segment proxy: OK")
def test_player_page(test_servers):
"""Test player page renders"""
video_url = f"http://127.0.0.1:{TEST_HTTP_PORT}/index.m3u8"
player_url = f"http://127.0.0.1:{SERVER_PORT}/player?url={urllib.parse.quote(video_url, safe='')}"
response = requests.get(player_url, timeout=10)
assert response.status_code == 200
assert "video" in response.text.lower()
print("Player page: OK")
def test_index_page(test_servers):
"""Test index page renders"""
response = requests.get(f"http://127.0.0.1:{SERVER_PORT}/", timeout=10)
assert response.status_code == 200
assert "video" in response.text.lower()
print("Index page: OK")
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+113
View File
@@ -0,0 +1,113 @@
import pytest
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import is_valid_url, extract_video_id, sanitize_path, get_error_message
import dlp
class TestURLValidation:
def test_valid_youtube_url(self):
assert is_valid_url("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
assert is_valid_url("https://youtu.be/dQw4w9WgXcQ")
def test_valid_youtu_be(self):
assert is_valid_url("https://youtu.be/abc123")
def test_valid_pornhub_url(self):
assert is_valid_url("https://www.pornhub.com/view_video.php?viewkey=abc123")
def test_invalid_url(self):
assert not is_valid_url("")
assert not is_valid_url("not-a-url")
def test_disallowed_domain(self):
os.environ["VALIDATION_ENABLED"] = "true"
assert not is_valid_url("https://evil.com/video")
class TestVideoIDExtraction:
def test_extract_youtube_id(self):
assert extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ") == "dQw4w9WgXcQ"
assert extract_video_id("https://youtu.be/dQw4w9WgXcQ") == "dQw4w9WgXcQ"
def test_extract_pornhub_id(self):
result = extract_video_id("https://www.pornhub.com/view_video.php?viewkey=ph123456")
assert result == "ph123456"
def test_extract_invalid(self):
assert extract_video_id("https://example.com/video") == ""
class TestPathSanitization:
def test_sanitize_normal_path(self):
assert sanitize_path("path/to/file") == "path/to/file"
def test_sanitize_prevents_traversal(self):
assert sanitize_path("../etc/passwd") == "etc/passwd"
assert sanitize_path("path/../etc/passwd") == "path/etc/passwd"
class TestCacheMechanics:
def test_cache_basic(self):
dlp._session_cache.clear()
dlp._cache_timestamps.clear()
test_data = {"test": "data"}
dlp._set_cached_session("http://test.com/video", test_data)
cached = dlp._get_cached_session("http://test.com/video")
assert cached == test_data
def test_cache_expiry(self):
dlp.CACHE_TTL = 1
dlp._session_cache.clear()
dlp._cache_timestamps.clear()
dlp._set_cached_session("http://test.com/video", {"data": "test"})
import time
time.sleep(1.1)
assert dlp._is_cache_expired("http://test.com/video") is True
dlp.CACHE_TTL = 31536000
class TestErrorMessages:
def test_get_error_message(self):
assert "Bad Request" in get_error_message(400)
assert "Forbidden" in get_error_message(403)
assert "Not Found" in get_error_message(404)
assert "Internal Server Error" in get_error_message(500)
class TestFlaskApp:
def test_index_route(self):
from app import app
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
def test_player_route_missing_url(self):
from app import app
with app.test_client() as client:
response = client.get("/player")
assert response.status_code == 400
def test_player_route_invalid_url(self):
from app import app
with app.test_client() as client:
response = client.get("/player?url=https://evil.com/video")
assert response.status_code == 400
def test_hls_proxy_invalid_path(self):
from app import app
with app.test_client() as client:
response = client.get("/hls")
assert response.status_code == 400
if __name__ == "__main__":
pytest.main([__file__, "-v"])