first commit
This commit is contained in:
43
src/encodeurls.py
Normal file
43
src/encodeurls.py
Normal 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
6
src/requirements.txt
Normal 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
124
src/stream_link_server.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user