first commit

This commit is contained in:
2025-02-23 20:31:57 -06:00
commit 34b4252adf
9 changed files with 366 additions and 0 deletions

43
src/encodeurls.py Normal file
View File

@@ -0,0 +1,43 @@
import urllib.parse
def encode_url_part(url):
"""Encodes the URL part, preserving URL special characters."""
return urllib.parse.quote(url, safe='~()*!.\'') # Extended safe characters to handle more URL characters
def process_m3u_file(input_file, output_file):
"""Reads the M3U file, encodes the stream URLs, and writes to a new file."""
base_url = 'http://192.168.1.123:6090/stream?url='
with open(input_file, 'r') as file:
lines = file.readlines()
with open(output_file, 'w') as file:
for line in lines:
line = line.strip()
if line.startswith('#EXTINF'):
# Write the #EXTINF line unchanged
file.write(line + '\n')
elif line.startswith(base_url):
# Extract and encode only the part after 'url='
url_part = line[len(base_url):]
print(f"Original URL part: {url_part}") # Debugging statement
# URL decode the part before encoding
url_part = urllib.parse.unquote(url_part)
print(f"Decoded URL part: {url_part}") # Debugging statement
encoded_url_part = encode_url_part(url_part)
print(f"Encoded URL part: {encoded_url_part}") # Debugging statement
encoded_line = f"{base_url}{encoded_url_part}"
file.write(encoded_line + '\n')
else:
# Write other lines unchanged
file.write(line + '\n')
# Define input and output file paths
input_file = 'input.m3u'
output_file = 'output_encoded.m3u'
# Process the M3U file
process_m3u_file(input_file, output_file)

6
src/requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
fastapi>=0.109.0
uvicorn>=0.27.0
streamlink>=6.5.0
python-multipart>=0.0.9
typing-extensions>=4.9.0
python-dotenv==1.0.0

124
src/stream_link_server.py Normal file
View File

@@ -0,0 +1,124 @@
import asyncio
from asyncio import create_subprocess_exec, subprocess
from asyncio.subprocess import Process
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from dotenv import load_dotenv
import uvicorn
import subprocess
import json
import os
from typing import Dict, List, Optional
# Load environment variables
load_dotenv()
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')
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 True
# Load channels configuration
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) -> Process:
"""
Run Streamlink as an async subprocess and pipe its output to the response.
"""
process = await create_subprocess_exec(
'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',
'--stdout',
url,
'best',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
return process
async def stream_generator(process: Process, url: str):
"""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)
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)
finally:
try:
process.terminate()
except:
pass
@app.get("/channels", response_model=List[Dict])
async def list_channels(auth: bool = Depends(verify_credentials)):
"""List all available channels"""
channels = load_channels()
return [{
'id': c['id'],
'name': c['name']
} for c in channels.values()]
@app.get("/{channel_id}")
async def stream_channel(channel_id: str, auth: bool = 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']
try:
process = await generate_streamlink_process(url)
return StreamingResponse(
stream_generator(process, url),
media_type='video/mp2t'
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error starting Streamlink: {str(e)}")
if __name__ == "__main__":
uvicorn.run(
"stream_link_server:app",
host="0.0.0.0",
port=6090,
reload=True,
workers=2
)