mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
234 lines
8.5 KiB
Python
234 lines
8.5 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 Movie, Episode
|
|
from dispatcharr.utils import network_access_allowed, get_client_ip
|
|
from core.models import UserAgent, CoreSettings
|
|
from .connection_manager import get_connection_manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["GET"])
|
|
def stream_movie(request, movie_uuid):
|
|
"""Stream movie content with connection tracking and range support"""
|
|
return _stream_content(request, Movie, movie_uuid, "movie")
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["GET"])
|
|
def stream_episode(request, episode_uuid):
|
|
"""Stream episode content with connection tracking and range support"""
|
|
return _stream_content(request, Episode, episode_uuid, "episode")
|
|
|
|
|
|
def _stream_content(request, model_class, content_uuid, content_type_name):
|
|
"""Generic function to stream VOD content"""
|
|
|
|
if not network_access_allowed(request, "STREAMS"):
|
|
return JsonResponse({"error": "Forbidden"}, status=403)
|
|
|
|
# Get content object
|
|
content = get_object_or_404(model_class, uuid=content_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: {content.name}")
|
|
|
|
try:
|
|
# Get connection manager
|
|
connection_manager = get_connection_manager()
|
|
|
|
# Get available M3U profile for connection management
|
|
m3u_account = content.m3u_account
|
|
available_profile = None
|
|
|
|
for profile in m3u_account.profiles.filter(is_active=True):
|
|
current_connections = connection_manager.get_profile_connections(profile.id)
|
|
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 in Redis
|
|
connection_created = connection_manager.create_connection(
|
|
content_type=content_type_name,
|
|
content_uuid=str(content_uuid),
|
|
content_name=content.name,
|
|
client_id=client_id,
|
|
client_ip=client_ip,
|
|
user_agent=client_user_agent,
|
|
m3u_profile=available_profile
|
|
)
|
|
|
|
if not connection_created:
|
|
return JsonResponse(
|
|
{"error": "Failed to create connection tracking"},
|
|
status=503
|
|
)
|
|
|
|
# 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(
|
|
content.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_manager.remove_connection(content_type_name, str(content_uuid), client_id)
|
|
return JsonResponse(
|
|
{"error": f"Upstream server error: {response.status_code}"},
|
|
status=response.status_code
|
|
)
|
|
|
|
# Determine content type
|
|
content_type_header = 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
|
|
connection_manager.update_connection_activity(
|
|
content_type_name,
|
|
str(content_uuid),
|
|
client_id,
|
|
bytes_sent=len(chunk)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"[{client_id}] Streaming error: {e}")
|
|
finally:
|
|
# Clean up connection when streaming ends
|
|
connection_manager.remove_connection(content_type_name, str(content_uuid), client_id)
|
|
logger.info(f"[{client_id}] Connection cleaned up")
|
|
|
|
# Build response with appropriate headers
|
|
streaming_response = StreamingHttpResponse(
|
|
stream_generator(),
|
|
content_type=content_type_header,
|
|
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 {content_type_name}: {content.name}")
|
|
return streaming_response
|
|
|
|
except requests.RequestException as e:
|
|
logger.error(f"[{client_id}] Request error: {e}")
|
|
connection_manager.remove_connection(content_type_name, str(content_uuid), client_id)
|
|
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_movie_position(request, movie_uuid):
|
|
"""Update playback position for a movie"""
|
|
return _update_position(request, Movie, movie_uuid, "movie")
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["POST"])
|
|
def update_episode_position(request, episode_uuid):
|
|
"""Update playback position for an episode"""
|
|
return _update_position(request, Episode, episode_uuid, "episode")
|
|
|
|
|
|
def _update_position(request, model_class, content_uuid, content_type_name):
|
|
"""Generic function to update playback position"""
|
|
|
|
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:
|
|
content = get_object_or_404(model_class, uuid=content_uuid)
|
|
connection_manager = get_connection_manager()
|
|
|
|
# Update position in Redis
|
|
success = connection_manager.update_connection_activity(
|
|
content_type_name,
|
|
str(content_uuid),
|
|
client_id,
|
|
position_seconds=position
|
|
)
|
|
|
|
if not success:
|
|
return JsonResponse({"error": "Connection not found"}, status=404)
|
|
|
|
return JsonResponse({"status": "success"})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Position update error: {e}")
|
|
return JsonResponse({"error": "Internal server error"}, status=500)
|