Dispatcharr/apps/proxy/vod_proxy/views.py
2025-08-02 10:42:36 -05:00

194 lines
6.9 KiB
Python

import time
import random
import logging
import requests
from django.http import StreamingHttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
from apps.vod.models import VOD, VODConnection
from apps.m3u.models import M3UAccountProfile
from dispatcharr.utils import network_access_allowed, get_client_ip
from core.models import UserAgent, CoreSettings
logger = logging.getLogger(__name__)
@csrf_exempt
@api_view(["GET"])
def stream_vod(request, vod_uuid):
"""Stream VOD content with connection tracking and range support"""
if not network_access_allowed(request, "STREAMS"):
return JsonResponse({"error": "Forbidden"}, status=403)
# Get VOD object
vod = get_object_or_404(VOD, uuid=vod_uuid)
# Generate client ID and get client info
client_id = f"vod_client_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
client_ip = get_client_ip(request)
client_user_agent = request.META.get('HTTP_USER_AGENT', '')
logger.info(f"[{client_id}] VOD stream request for: {vod.name}")
try:
# Get available M3U profile for connection management
m3u_account = vod.m3u_account
available_profile = None
for profile in m3u_account.profiles.filter(is_active=True):
current_connections = VODConnection.objects.filter(m3u_profile=profile).count()
if profile.max_streams == 0 or current_connections < profile.max_streams:
available_profile = profile
break
if not available_profile:
return JsonResponse(
{"error": "No available connections for this VOD"},
status=503
)
# Create connection tracking record
connection = VODConnection.objects.create(
vod=vod,
m3u_profile=available_profile,
client_id=client_id,
client_ip=client_ip,
user_agent=client_user_agent
)
# Get user agent for upstream request
try:
user_agent_obj = m3u_account.get_user_agent()
upstream_user_agent = user_agent_obj.user_agent
except:
default_ua_id = CoreSettings.get_default_user_agent_id()
user_agent_obj = UserAgent.objects.get(id=default_ua_id)
upstream_user_agent = user_agent_obj.user_agent
# Handle range requests for seeking
range_header = request.META.get('HTTP_RANGE')
headers = {
'User-Agent': upstream_user_agent,
'Connection': 'keep-alive'
}
if range_header:
headers['Range'] = range_header
logger.debug(f"[{client_id}] Range request: {range_header}")
# Stream the VOD content
try:
response = requests.get(
vod.url,
headers=headers,
stream=True,
timeout=(10, 60)
)
if response.status_code not in [200, 206]:
logger.error(f"[{client_id}] Upstream error: {response.status_code}")
connection.delete()
return JsonResponse(
{"error": f"Upstream server error: {response.status_code}"},
status=response.status_code
)
# Determine content type
content_type = response.headers.get('Content-Type', 'video/mp4')
content_length = response.headers.get('Content-Length')
content_range = response.headers.get('Content-Range')
# Create streaming response
def stream_generator():
bytes_sent = 0
try:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
bytes_sent += len(chunk)
yield chunk
# Update connection activity periodically
if bytes_sent % (8192 * 10) == 0: # Every ~80KB
try:
connection.update_activity(bytes_sent=len(chunk))
except VODConnection.DoesNotExist:
# Connection was cleaned up, stop streaming
break
except Exception as e:
logger.error(f"[{client_id}] Streaming error: {e}")
finally:
# Clean up connection when streaming ends
try:
connection.delete()
logger.info(f"[{client_id}] Connection cleaned up")
except VODConnection.DoesNotExist:
pass
# Build response with appropriate headers
streaming_response = StreamingHttpResponse(
stream_generator(),
content_type=content_type,
status=response.status_code
)
# Copy important headers
if content_length:
streaming_response['Content-Length'] = content_length
if content_range:
streaming_response['Content-Range'] = content_range
# Add CORS and caching headers
streaming_response['Accept-Ranges'] = 'bytes'
streaming_response['Access-Control-Allow-Origin'] = '*'
streaming_response['Cache-Control'] = 'no-cache'
logger.info(f"[{client_id}] Started streaming VOD: {vod.name}")
return streaming_response
except requests.RequestException as e:
logger.error(f"[{client_id}] Request error: {e}")
connection.delete()
return JsonResponse(
{"error": "Failed to connect to upstream server"},
status=502
)
except Exception as e:
logger.error(f"[{client_id}] Unexpected error: {e}")
return JsonResponse(
{"error": "Internal server error"},
status=500
)
@csrf_exempt
@api_view(["POST"])
def update_position(request, vod_uuid):
"""Update playback position for a VOD"""
if not network_access_allowed(request, "STREAMS"):
return JsonResponse({"error": "Forbidden"}, status=403)
client_id = request.data.get('client_id')
position = request.data.get('position', 0)
if not client_id:
return JsonResponse({"error": "Client ID required"}, status=400)
try:
vod = get_object_or_404(VOD, uuid=vod_uuid)
connection = VODConnection.objects.get(vod=vod, client_id=client_id)
connection.update_activity(position=position)
return JsonResponse({"status": "success"})
except VODConnection.DoesNotExist:
return JsonResponse({"error": "Connection not found"}, status=404)
except Exception as e:
logger.error(f"Position update error: {e}")
return JsonResponse({"error": "Internal server error"}, status=500)