Code refactoring. Added router and routes. Added static content.
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -2,9 +2,11 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"fout",
|
"fout",
|
||||||
|
"levelname",
|
||||||
"mpegts",
|
"mpegts",
|
||||||
"Referer",
|
"Referer",
|
||||||
"ringbuffer",
|
"ringbuffer",
|
||||||
"streamlink"
|
"streamlink",
|
||||||
|
"uvicorn"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -17,4 +17,4 @@ RUN git clone https://git.fiorinis.com/Home/streamlink-server.git .
|
|||||||
RUN pip install --no-cache-dir -r ./src/requirements.txt
|
RUN pip install --no-cache-dir -r ./src/requirements.txt
|
||||||
|
|
||||||
EXPOSE 7860
|
EXPOSE 7860
|
||||||
CMD ["uvicorn", "run:main_app", "--host", "0.0.0.0", "--reload", "--port", "7860", "--workers", "4"]
|
CMD ["uvicorn", "src.stream_link_server:app", "--host", "0.0.0.0", "--reload", "--port", "7860", "--workers", "4"]
|
||||||
22
run.py
22
run.py
@@ -1,21 +1,19 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add the src directory to Python path
|
# Add the src directory to Python path
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||||
|
|
||||||
from fastapi import FastAPI
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
from src.stream_link_server import app as streamlink_server_app
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Initialize the main FastAPI application
|
|
||||||
main_app = FastAPI()
|
|
||||||
|
|
||||||
# Manually add only non-static routes from streamlink_server_app
|
|
||||||
for route in streamlink_server_app.routes:
|
|
||||||
if route.path != "/": # Exclude the static file path
|
|
||||||
main_app.router.routes.append(route)
|
|
||||||
|
|
||||||
# Run the main app
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(main_app, host="0.0.0.0", port=8080)
|
uvicorn.run(
|
||||||
|
"src.stream_link_server:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8080,
|
||||||
|
reload=True,
|
||||||
|
workers=2
|
||||||
|
)
|
||||||
0
src/routes/__init__.py
Normal file
0
src/routes/__init__.py
Normal file
93
src/routes/stream.py
Normal file
93
src/routes/stream.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import Depends, HTTPException, APIRouter, Request
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from utils.file_utils import load_channels
|
||||||
|
from utils.stream_utils import generate_streamlink_process, stream_generator
|
||||||
|
|
||||||
|
stream_router = APIRouter()
|
||||||
|
|
||||||
|
def get_data_dir(request: Request) -> str:
|
||||||
|
try:
|
||||||
|
return request.app.state.DATA_DIR
|
||||||
|
except AttributeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server configuration error: DATA_DIR not initialized"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_channels_file_name(request: Request) -> str:
|
||||||
|
try:
|
||||||
|
return request.app.state.CHANNELS_FILE_NAME
|
||||||
|
except AttributeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server configuration error: CHANNELS_FILE_NAME not initialized"
|
||||||
|
)
|
||||||
|
|
||||||
|
@stream_router.get("/url")
|
||||||
|
async def stream_custom(
|
||||||
|
url: str,
|
||||||
|
origin: Optional[str] = None,
|
||||||
|
referer: Optional[str] = None,
|
||||||
|
agent: Optional[str] = None,
|
||||||
|
proxy: Optional[str] = None
|
||||||
|
):
|
||||||
|
"""Stream directly from query parameters"""
|
||||||
|
headers = {}
|
||||||
|
if origin:
|
||||||
|
headers['Origin'] = origin
|
||||||
|
if referer:
|
||||||
|
headers['Referer'] = referer
|
||||||
|
if agent:
|
||||||
|
headers['User-Agent'] = agent
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = await generate_streamlink_process(
|
||||||
|
url,
|
||||||
|
headers if headers else None,
|
||||||
|
proxy if proxy else None
|
||||||
|
)
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_generator(process, url, headers, proxy),
|
||||||
|
media_type='video/mp2t'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error starting Streamlink: {str(e)}")
|
||||||
|
|
||||||
|
@stream_router.get("/{channel_id}")
|
||||||
|
async def stream_channel(
|
||||||
|
channel_id: str,
|
||||||
|
data_dir: str = Depends(get_data_dir),
|
||||||
|
channels_file_name: str = Depends(get_channels_file_name)
|
||||||
|
):
|
||||||
|
|
||||||
|
"""Stream a channel by ID"""
|
||||||
|
channels = load_channels(data_dir, channels_file_name)
|
||||||
|
if channel_id not in channels:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
|
||||||
|
channel = channels[channel_id]
|
||||||
|
url = channel['url']
|
||||||
|
|
||||||
|
# Build headers dict only including specified headers
|
||||||
|
headers = {}
|
||||||
|
if 'origin' in channel:
|
||||||
|
headers['Origin'] = channel['origin']
|
||||||
|
if 'referer' in channel:
|
||||||
|
headers['Referer'] = channel['referer']
|
||||||
|
if 'agent' in channel:
|
||||||
|
headers['User-Agent'] = channel['agent']
|
||||||
|
|
||||||
|
# Get proxy if specified
|
||||||
|
proxy = channel.get('proxy')
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = await generate_streamlink_process(url, headers if headers else None, proxy if proxy else None)
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_generator(process, url, headers if headers else None, proxy if proxy else None),
|
||||||
|
media_type='video/mp2t'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error starting Streamlink: {str(e)}")
|
||||||
66
src/routes/utils.py
Normal file
66
src/routes/utils.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from fastapi import Depends, APIRouter, Request, HTTPException
|
||||||
|
from typing import Dict, List
|
||||||
|
from schema import GenerateUrlRequest
|
||||||
|
from utils.file_utils import load_channels, verify_credentials
|
||||||
|
from utils.http_utils import encode_streamlink_server_url
|
||||||
|
from utils.stream_utils import generate_streamlink_process, stream_generator
|
||||||
|
|
||||||
|
utils_router = APIRouter()
|
||||||
|
|
||||||
|
def get_data_dir(request: Request) -> str:
|
||||||
|
try:
|
||||||
|
return request.app.state.DATA_DIR
|
||||||
|
except AttributeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server configuration error: DATA_DIR not initialized"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_channels_file_name(request: Request) -> str:
|
||||||
|
try:
|
||||||
|
return request.app.state.CHANNELS_FILE_NAME
|
||||||
|
except AttributeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Server configuration error: CHANNELS_FILE_NAME not initialized"
|
||||||
|
)
|
||||||
|
|
||||||
|
@utils_router.get("/channels", response_model=List[Dict])
|
||||||
|
async def list_channels(
|
||||||
|
data_dir: str = Depends(get_data_dir),
|
||||||
|
channels_file_name: str = Depends(get_channels_file_name)):
|
||||||
|
|
||||||
|
"""List all available channels"""
|
||||||
|
channels = load_channels(data_dir, channels_file_name)
|
||||||
|
return [{
|
||||||
|
'id': c['id'],
|
||||||
|
'name': c['name']
|
||||||
|
} for c in channels.values()]
|
||||||
|
|
||||||
|
@utils_router.post(
|
||||||
|
"/generate_url",
|
||||||
|
description="Generate a single encoded URL",
|
||||||
|
response_description="Returns a single encoded URL",
|
||||||
|
)
|
||||||
|
async def generate_url(
|
||||||
|
request: GenerateUrlRequest,
|
||||||
|
http_request: Request,
|
||||||
|
credentials: tuple = Depends(verify_credentials)):
|
||||||
|
"""Generate a single encoded URL based on the provided request."""
|
||||||
|
|
||||||
|
username, password = credentials
|
||||||
|
|
||||||
|
encoded_url = encode_streamlink_server_url(
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
base_url=str(http_request.base_url),
|
||||||
|
stream_url=request.stream_url,
|
||||||
|
endpoint=request.endpoint,
|
||||||
|
agent=request.agent,
|
||||||
|
proxy_url=request.proxy_url,
|
||||||
|
request_headers=request.request_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"url": encoded_url}
|
||||||
76
src/static/index.html
Normal file
76
src/static/index.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MediaFlow Proxy</title>
|
||||||
|
<link rel="icon" href="/logo.png" type="image/x-icon">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: #90aacc;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 20px;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<img src="/logo.png" alt="MediaFlow Proxy Logo">
|
||||||
|
<h1>MediaFlow Proxy</h1>
|
||||||
|
</header>
|
||||||
|
<p>A high-performance proxy server for streaming media, supporting HTTP(S), HLS, and MPEG-DASH with real-time DRM decryption.</p>
|
||||||
|
|
||||||
|
<h2>Key Features</h2>
|
||||||
|
<div class="feature">Convert MPEG-DASH streams (DRM-protected and non-protected) to HLS</div>
|
||||||
|
<div class="feature">Support for Clear Key DRM-protected MPD DASH streams</div>
|
||||||
|
<div class="feature">Handle both live and video-on-demand (VOD) DASH streams</div>
|
||||||
|
<div class="feature">Proxy HTTP/HTTPS links with custom headers</div>
|
||||||
|
<div class="feature">Proxy and modify HLS (M3U8) streams in real-time with custom headers and key URL modifications for bypassing some sneaky restrictions.</div>
|
||||||
|
<div class="feature">Protect against unauthorized access and network bandwidth abuses</div>
|
||||||
|
|
||||||
|
<h2>Getting Started</h2>
|
||||||
|
<p>Visit the <a href="https://github.com/mhdzumair/mediaflow-proxy">GitHub repository</a> for installation instructions and documentation.</p>
|
||||||
|
|
||||||
|
<h2>Premium Hosted Service</h2>
|
||||||
|
<p>For a hassle-free experience, check out <a href="https://store.elfhosted.com/product/mediaflow-proxy">premium hosted service on ElfHosted</a>.</p>
|
||||||
|
|
||||||
|
<h2>API Documentation</h2>
|
||||||
|
<p>Explore the <a href="/docs">Swagger UI</a> for comprehensive details about the API endpoints and their usage.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
src/static/logo.png
Normal file
BIN
src/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -1,210 +1,55 @@
|
|||||||
import asyncio
|
from contextlib import asynccontextmanager
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from asyncio import create_subprocess_exec, subprocess
|
import logging
|
||||||
from asyncio.subprocess import Process
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from typing import Dict, List, Optional
|
from routes.stream import stream_router
|
||||||
from schema import GenerateUrlRequest
|
from routes.utils import utils_router
|
||||||
from utils.http_utils import encode_streamlink_server_url
|
from utils.file_utils import load_channels, verify_credentials
|
||||||
|
from starlette.responses import RedirectResponse
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
from importlib import resources
|
||||||
|
|
||||||
# Load environment variables
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
load_dotenv()
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = FastAPI(title="Streamlink Server")
|
|
||||||
|
|
||||||
AUTH_USERNAME = os.getenv("AUTH_USERNAME")
|
|
||||||
AUTH_PASSWORD = os.getenv("AUTH_PASSWORD")
|
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
|
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
|
||||||
|
CHANNELS_FILE_NAME = 'channels.json'
|
||||||
|
|
||||||
def verify_credentials(username: Optional[str] = None, password: Optional[str] = None):
|
@asynccontextmanager
|
||||||
if not username or not password:
|
async def lifespan(app: FastAPI):
|
||||||
raise HTTPException(status_code=401, detail="Authentication required")
|
if not os.path.exists(DATA_DIR):
|
||||||
|
os.makedirs(DATA_DIR)
|
||||||
|
|
||||||
if username != AUTH_USERNAME or password != AUTH_PASSWORD:
|
app.state.DATA_DIR = DATA_DIR
|
||||||
raise HTTPException(status_code=403, detail="Invalid credentials")
|
app.state.CHANNELS_FILE_NAME = CHANNELS_FILE_NAME
|
||||||
|
|
||||||
return username, password
|
logger.info(f"Initialized app state with DATA_DIR: {DATA_DIR}")
|
||||||
|
yield
|
||||||
|
logger.info("Application shutting down...")
|
||||||
|
|
||||||
# Load channels configuration
|
app = FastAPI(title="Streamlink Server", lifespan=lifespan)
|
||||||
def load_channels():
|
|
||||||
"""Load channels configuration from JSON file"""
|
|
||||||
channels_path = os.path.join(DATA_DIR, 'channels.json')
|
|
||||||
with open(channels_path, 'r') as f:
|
|
||||||
return {str(c['id']): c for c in json.load(f)['channels']}
|
|
||||||
|
|
||||||
async def generate_streamlink_process(url, headers=None, proxy=None) -> Process:
|
@app.get("/health")
|
||||||
"""
|
async def health_check():
|
||||||
Run Streamlink as an async subprocess and pipe its output to the response.
|
return {"status": "healthy"}
|
||||||
Args:
|
|
||||||
url: Stream URL
|
|
||||||
headers: Optional dict of HTTP headers
|
|
||||||
"""
|
|
||||||
cmd = [
|
|
||||||
'streamlink',
|
|
||||||
'--ffmpeg-fout', 'mpegts',
|
|
||||||
'--hls-live-restart',
|
|
||||||
'--retry-streams', '3',
|
|
||||||
'--stream-timeout', '60',
|
|
||||||
'--hls-playlist-reload-attempts', '3',
|
|
||||||
'--stream-segment-threads', '3',
|
|
||||||
'--ringbuffer-size', '32M',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add proxy if specified
|
@app.get("/favicon.ico")
|
||||||
if proxy:
|
async def get_favicon():
|
||||||
cmd.extend(['--http-proxy', proxy])
|
return RedirectResponse(url="/logo.png")
|
||||||
|
|
||||||
# Add headers if specified
|
app.include_router(stream_router, prefix="/stream", tags=["stream"], dependencies=[Depends(verify_credentials)])
|
||||||
if headers:
|
app.include_router(utils_router, prefix="/utils", tags=["utils"], dependencies=[Depends(verify_credentials)])
|
||||||
for key, value in headers.items():
|
|
||||||
cmd.extend(['--http-header', f'{key}={value}'])
|
|
||||||
|
|
||||||
cmd.extend(['--stdout', url, 'best'])
|
static_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
||||||
|
app.mount("/", StaticFiles(directory=static_path, html=True), name="static")
|
||||||
process = await create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE
|
|
||||||
)
|
|
||||||
return process
|
|
||||||
|
|
||||||
async def stream_generator(process: Process, url: str, headers=None, proxy=None):
|
|
||||||
"""Generate streaming content asynchronously"""
|
|
||||||
CHUNK_SIZE = 32768
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
if process.returncode is not None:
|
|
||||||
# Process has terminated, restart it
|
|
||||||
process = await generate_streamlink_process(url, headers, proxy)
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = await process.stdout.read(CHUNK_SIZE)
|
|
||||||
if output:
|
|
||||||
yield output
|
|
||||||
else:
|
|
||||||
# No output but process still running, wait briefly
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading stream: {e}")
|
|
||||||
try:
|
|
||||||
process.terminate()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
process = await generate_streamlink_process(url, headers, proxy)
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
process.terminate()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@app.post(
|
|
||||||
"/generate_url",
|
|
||||||
description="Generate a single encoded URL",
|
|
||||||
response_description="Returns a single encoded URL",
|
|
||||||
)
|
|
||||||
async def generate_url(
|
|
||||||
request: GenerateUrlRequest,
|
|
||||||
http_request: Request,
|
|
||||||
credentials: tuple = Depends(verify_credentials)):
|
|
||||||
"""Generate a single encoded URL based on the provided request."""
|
|
||||||
|
|
||||||
username, password = credentials
|
|
||||||
|
|
||||||
encoded_url = encode_streamlink_server_url(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
base_url=str(http_request.base_url),
|
|
||||||
stream_url=request.stream_url,
|
|
||||||
endpoint=request.endpoint,
|
|
||||||
agent=request.agent,
|
|
||||||
proxy_url=request.proxy_url,
|
|
||||||
request_headers=request.request_headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"url": encoded_url}
|
|
||||||
|
|
||||||
@app.get("/channels", response_model=List[Dict])
|
|
||||||
async def list_channels(credentials: tuple = Depends(verify_credentials)):
|
|
||||||
"""List all available channels"""
|
|
||||||
channels = load_channels()
|
|
||||||
return [{
|
|
||||||
'id': c['id'],
|
|
||||||
'name': c['name']
|
|
||||||
} for c in channels.values()]
|
|
||||||
|
|
||||||
@app.get("/stream")
|
|
||||||
async def stream_custom(
|
|
||||||
url: str,
|
|
||||||
origin: Optional[str] = None,
|
|
||||||
referer: Optional[str] = None,
|
|
||||||
agent: Optional[str] = None,
|
|
||||||
proxy: Optional[str] = None,
|
|
||||||
credentials: tuple = Depends(verify_credentials)
|
|
||||||
):
|
|
||||||
"""Stream directly from query parameters"""
|
|
||||||
headers = {}
|
|
||||||
if origin:
|
|
||||||
headers['Origin'] = origin
|
|
||||||
if referer:
|
|
||||||
headers['Referer'] = referer
|
|
||||||
if agent:
|
|
||||||
headers['User-Agent'] = agent
|
|
||||||
|
|
||||||
try:
|
|
||||||
process = await generate_streamlink_process(
|
|
||||||
url,
|
|
||||||
headers if headers else None,
|
|
||||||
proxy if proxy else None
|
|
||||||
)
|
|
||||||
return StreamingResponse(
|
|
||||||
stream_generator(process, url, headers, proxy),
|
|
||||||
media_type='video/mp2t'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error starting Streamlink: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/{channel_id}")
|
|
||||||
async def stream_channel(channel_id: str, credentials: tuple = Depends(verify_credentials)):
|
|
||||||
"""Stream a channel by ID"""
|
|
||||||
channels = load_channels()
|
|
||||||
if channel_id not in channels:
|
|
||||||
raise HTTPException(status_code=404, detail="Channel not found")
|
|
||||||
|
|
||||||
channel = channels[channel_id]
|
|
||||||
url = channel['url']
|
|
||||||
|
|
||||||
# Build headers dict only including specified headers
|
|
||||||
headers = {}
|
|
||||||
if 'origin' in channel:
|
|
||||||
headers['Origin'] = channel['origin']
|
|
||||||
if 'referer' in channel:
|
|
||||||
headers['Referer'] = channel['referer']
|
|
||||||
if 'agent' in channel:
|
|
||||||
headers['User-Agent'] = channel['agent']
|
|
||||||
|
|
||||||
# Get proxy if specified
|
|
||||||
proxy = channel.get('proxy')
|
|
||||||
|
|
||||||
try:
|
|
||||||
process = await generate_streamlink_process(url, headers if headers else None, proxy if proxy else None)
|
|
||||||
return StreamingResponse(
|
|
||||||
stream_generator(process, url, headers if headers else None, proxy if proxy else None),
|
|
||||||
media_type='video/mp2t'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error starting Streamlink: {str(e)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"stream_link_server:app",
|
"stream_link_server:app",
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=6090,
|
port=8080,
|
||||||
reload=True,
|
reload=True,
|
||||||
workers=2
|
workers=2
|
||||||
)
|
)
|
||||||
|
|||||||
28
src/utils/file_utils.py
Normal file
28
src/utils/file_utils.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
AUTH_USERNAME = os.getenv("AUTH_USERNAME")
|
||||||
|
AUTH_PASSWORD = os.getenv("AUTH_PASSWORD")
|
||||||
|
|
||||||
|
# Credentials verification
|
||||||
|
def verify_credentials(username: Optional[str] = None, password: Optional[str] = None):
|
||||||
|
if not username or not password:
|
||||||
|
raise HTTPException(status_code=401, detail="Authentication required")
|
||||||
|
|
||||||
|
if username != AUTH_USERNAME or password != AUTH_PASSWORD:
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid credentials")
|
||||||
|
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
# Load channels configuration
|
||||||
|
def load_channels(data_dir: str = 'data', filename: str = 'channels.json') -> dict:
|
||||||
|
"""Load channels configuration from JSON file"""
|
||||||
|
channels_path = os.path.join(data_dir, filename)
|
||||||
|
with open(channels_path, 'r') as f:
|
||||||
|
return {str(c['id']): c for c in json.load(f)['channels']}
|
||||||
70
src/utils/stream_utils.py
Normal file
70
src/utils/stream_utils.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
import asyncio
|
||||||
|
from asyncio import create_subprocess_exec, subprocess
|
||||||
|
from asyncio.subprocess import Process
|
||||||
|
|
||||||
|
async def stream_generator(process: Process, url: str, headers=None, proxy=None):
|
||||||
|
"""Generate streaming content asynchronously"""
|
||||||
|
CHUNK_SIZE = 32768
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if process.returncode is not None:
|
||||||
|
# Process has terminated, restart it
|
||||||
|
process = await generate_streamlink_process(url, headers, proxy)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = await process.stdout.read(CHUNK_SIZE)
|
||||||
|
if output:
|
||||||
|
yield output
|
||||||
|
else:
|
||||||
|
# No output but process still running, wait briefly
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading stream: {e}")
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
process = await generate_streamlink_process(url, headers, proxy)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def generate_streamlink_process(url, headers=None, proxy=None) -> Process:
|
||||||
|
"""
|
||||||
|
Run Streamlink as an async subprocess and pipe its output to the response.
|
||||||
|
Args:
|
||||||
|
url: Stream URL
|
||||||
|
headers: Optional dict of HTTP headers
|
||||||
|
"""
|
||||||
|
cmd = [
|
||||||
|
'streamlink',
|
||||||
|
'--ffmpeg-fout', 'mpegts',
|
||||||
|
'--hls-live-restart',
|
||||||
|
'--retry-streams', '3',
|
||||||
|
'--stream-timeout', '60',
|
||||||
|
'--hls-playlist-reload-attempts', '3',
|
||||||
|
'--stream-segment-threads', '3',
|
||||||
|
'--ringbuffer-size', '32M',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add proxy if specified
|
||||||
|
if proxy:
|
||||||
|
cmd.extend(['--http-proxy', proxy])
|
||||||
|
|
||||||
|
# Add headers if specified
|
||||||
|
if headers:
|
||||||
|
for key, value in headers.items():
|
||||||
|
cmd.extend(['--http-header', f'{key}={value}'])
|
||||||
|
|
||||||
|
cmd.extend(['--stdout', url, 'best'])
|
||||||
|
|
||||||
|
process = await create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
return process
|
||||||
Reference in New Issue
Block a user