Fix vod streaming.

This commit is contained in:
SergeantPanda 2025-08-08 08:35:59 -05:00
parent 22bc573c10
commit 345247df11
4 changed files with 176 additions and 68 deletions

View file

@ -17,6 +17,15 @@ logger = logging.getLogger("vod_proxy")
class VODConnectionManager:
"""Manages VOD connections using Redis for tracking"""
_instance = None
@classmethod
def get_instance(cls):
"""Get the singleton instance of VODConnectionManager"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
self.redis_client = RedisClient.get_client()
self.connection_ttl = 3600 # 1 hour TTL for connections
@ -295,6 +304,109 @@ class VODConnectionManager:
except Exception as e:
logger.error(f"Error during connection cleanup: {e}")
def stream_content(self, content_obj, stream_url, m3u_profile, client_ip, user_agent, request):
"""
Stream VOD content with connection tracking
Args:
content_obj: Movie or Episode object
stream_url: Final stream URL to proxy
m3u_profile: M3UAccountProfile instance
client_ip: Client IP address
user_agent: Client user agent
request: Django request object
Returns:
StreamingHttpResponse or HttpResponse with error
"""
import time
import random
import requests
from django.http import StreamingHttpResponse, HttpResponse
try:
# Generate unique client ID
client_id = f"vod_{int(time.time() * 1000)}_{random.randint(1000, 9999)}"
# Determine content type and get content info
if hasattr(content_obj, 'episodes'): # Series
content_type = 'series'
elif hasattr(content_obj, 'series'): # Episode
content_type = 'episode'
else: # Movie
content_type = 'movie'
content_uuid = str(content_obj.uuid)
content_name = getattr(content_obj, 'name', getattr(content_obj, 'title', 'Unknown'))
# Create connection tracking
connection_created = self.create_connection(
content_type=content_type,
content_uuid=content_uuid,
content_name=content_name,
client_id=client_id,
client_ip=client_ip,
user_agent=user_agent,
m3u_profile=m3u_profile
)
if not connection_created:
logger.error(f"Failed to create connection tracking for {content_type} {content_uuid}")
return HttpResponse("Connection limit exceeded", status=503)
# Create streaming generator
def stream_generator():
try:
logger.info(f"[{client_id}] Starting VOD stream for {content_type} {content_name}")
# Make request to actual stream URL
headers = {'User-Agent': user_agent} if user_agent else {}
with requests.get(stream_url, headers=headers, stream=True, timeout=(10, 30)) as response:
response.raise_for_status()
bytes_sent = 0
chunk_count = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
yield chunk
bytes_sent += len(chunk)
chunk_count += 1
# Update connection activity every 100 chunks
if chunk_count % 100 == 0:
self.update_connection_activity(
content_type=content_type,
content_uuid=content_uuid,
client_id=client_id,
bytes_sent=len(chunk)
)
logger.info(f"[{client_id}] VOD stream completed: {bytes_sent} bytes sent")
except requests.RequestException as e:
logger.error(f"[{client_id}] Error streaming from source: {e}")
yield b"Error: Unable to stream content"
except Exception as e:
logger.error(f"[{client_id}] Error in stream generator: {e}")
finally:
# Clean up connection tracking
self.remove_connection(content_type, content_uuid, client_id)
# Create streaming response
response = StreamingHttpResponse(
streaming_content=stream_generator(),
content_type='video/mp4'
)
response['Cache-Control'] = 'no-cache'
response['Accept-Ranges'] = 'none'
return response
except Exception as e:
logger.error(f"Error in stream_content: {e}", exc_info=True)
return HttpResponse(f"Streaming error: {str(e)}", status=500)
# Global instance
_connection_manager = None

View file

@ -5,7 +5,7 @@ app_name = 'vod_proxy'
urlpatterns = [
# Generic VOD streaming (supports movies, episodes, series)
path('<str:content_type>/<uuid:content_id>/', views.VODStreamView.as_view(), name='vod_stream'),
path('<str:content_type>/<uuid:content_id>', views.VODStreamView.as_view(), name='vod_stream'),
path('<str:content_type>/<uuid:content_id>/<int:profile_id>/', views.VODStreamView.as_view(), name='vod_stream_with_profile'),
# VOD playlist generation

View file

@ -41,19 +41,27 @@ class VODStreamView(View):
client_ip, user_agent = get_client_info(request)
logger.info(f"[VOD-CLIENT] Client info - IP: {client_ip}, User-Agent: {user_agent[:100]}...")
# Get the content object
content_obj = self._get_content_object(content_type, content_id)
if not content_obj:
logger.error(f"[VOD-ERROR] Content not found: {content_type} {content_id}")
# Get the content object and its relation
content_obj, relation = self._get_content_and_relation(content_type, content_id)
if not content_obj or not relation:
logger.error(f"[VOD-ERROR] Content or relation not found: {content_type} {content_id}")
raise Http404(f"Content not found: {content_type} {content_id}")
logger.info(f"[VOD-CONTENT] Found content: {content_obj.title if hasattr(content_obj, 'title') else getattr(content_obj, 'name', 'Unknown')}")
logger.info(f"[VOD-CONTENT] Content URL: {getattr(content_obj, 'url', 'No URL found')}")
logger.info(f"[VOD-CONTENT] Found content: {getattr(content_obj, 'name', 'Unknown')}")
# Get M3U account and profile
m3u_account = content_obj.m3u_account
# Get M3U account from relation
m3u_account = relation.m3u_account
logger.info(f"[VOD-ACCOUNT] Using M3U account: {m3u_account.name}")
# Get stream URL from relation
stream_url = self._get_stream_url_from_relation(relation)
logger.info(f"[VOD-CONTENT] Content URL: {stream_url or 'No URL found'}")
if not stream_url:
logger.error(f"[VOD-ERROR] No stream URL available for {content_type} {content_id}")
return HttpResponse("No stream URL available", status=503)
# Get M3U profile
m3u_profile = self._get_m3u_profile(m3u_account, profile_id, user_agent)
if not m3u_profile:
@ -73,12 +81,12 @@ class VODStreamView(View):
logger.error(f"Error tracking connection in Redis: {e}")
# Transform URL based on profile
stream_url = self._transform_url(content_obj, m3u_profile)
logger.info(f"[VOD-URL] Final stream URL: {stream_url}")
final_stream_url = self._transform_url(stream_url, m3u_profile)
logger.info(f"[VOD-URL] Final stream URL: {final_stream_url}")
# Validate stream URL
if not stream_url or not stream_url.startswith(('http://', 'https://')):
logger.error(f"[VOD-ERROR] Invalid stream URL: {stream_url}")
if not final_stream_url or not final_stream_url.startswith(('http://', 'https://')):
logger.error(f"[VOD-ERROR] Invalid stream URL: {final_stream_url}")
return HttpResponse("Invalid stream URL", status=500)
# Get connection manager
@ -88,7 +96,7 @@ class VODStreamView(View):
logger.info("[VOD-STREAM] Calling connection manager to stream content")
response = connection_manager.stream_content(
content_obj=content_obj,
stream_url=stream_url,
stream_url=final_stream_url,
m3u_profile=m3u_profile,
client_ip=client_ip,
user_agent=user_agent,
@ -102,18 +110,27 @@ class VODStreamView(View):
logger.error(f"[VOD-EXCEPTION] Error streaming {content_type} {content_id}: {e}", exc_info=True)
return HttpResponse(f"Streaming error: {str(e)}", status=500)
def _get_content_object(self, content_type, content_id):
"""Get the content object based on type and UUID"""
def _get_content_and_relation(self, content_type, content_id):
"""Get the content object and its M3U relation"""
try:
logger.info(f"[CONTENT-LOOKUP] Looking up {content_type} with UUID {content_id}")
if content_type == 'movie':
obj = get_object_or_404(Movie, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Movie: {obj.name} (ID: {obj.id})")
return obj
content_obj = get_object_or_404(Movie, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Movie: {content_obj.name} (ID: {content_obj.id})")
# Get the first active relation
relation = content_obj.m3u_relations.filter(m3u_account__is_active=True).first()
return content_obj, relation
elif content_type == 'episode':
obj = get_object_or_404(Episode, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Episode: {obj.name} (ID: {obj.id}, Series: {obj.series.name})")
return obj
content_obj = get_object_or_404(Episode, uuid=content_id)
logger.info(f"[CONTENT-FOUND] Episode: {content_obj.name} (ID: {content_obj.id}, Series: {content_obj.series.name})")
# Get the first active relation
relation = content_obj.m3u_relations.filter(m3u_account__is_active=True).first()
return content_obj, relation
elif content_type == 'series':
# For series, get the first episode
series = get_object_or_404(Series, uuid=content_id)
@ -121,37 +138,36 @@ class VODStreamView(View):
episode = series.episodes.first()
if not episode:
logger.error(f"[CONTENT-ERROR] No episodes found for series {series.name}")
raise Http404("No episodes found for series")
return None, None
logger.info(f"[CONTENT-FOUND] First episode: {episode.name} (ID: {episode.id})")
return episode
relation = episode.m3u_relations.filter(m3u_account__is_active=True).first()
return episode, relation
else:
logger.error(f"[CONTENT-ERROR] Invalid content type: {content_type}")
raise Http404(f"Invalid content type: {content_type}")
return None, None
except Exception as e:
logger.error(f"Error getting content object: {e}")
return None, None
def _get_stream_url_from_relation(self, relation):
"""Get stream URL from the M3U relation"""
try:
if hasattr(relation, 'url') and relation.url:
return relation.url
elif hasattr(relation, 'get_stream_url'):
return relation.get_stream_url()
else:
logger.error("Relation has no URL or get_stream_url method")
return None
except Exception as e:
logger.error(f"Error getting stream URL from relation: {e}")
return None
def _get_m3u_profile(self, content_obj, profile_id, user_agent):
def _get_m3u_profile(self, m3u_account, profile_id, user_agent):
"""Get appropriate M3U profile for streaming"""
try:
# Get M3U account from content object's relations
m3u_account = None
if hasattr(content_obj, 'm3u_relations'):
# This is a Movie or Episode with relations
relation = content_obj.m3u_relations.filter(m3u_account__is_active=True).first()
if relation:
m3u_account = relation.m3u_account
elif hasattr(content_obj, 'series'):
# This is an Episode, get relation through series
relation = content_obj.series.m3u_relations.filter(m3u_account__is_active=True).first()
if relation:
m3u_account = relation.m3u_account
if not m3u_account:
logger.error("No M3U account found for content object")
return None
# If specific profile requested, try to use it
if profile_id:
try:
@ -195,33 +211,12 @@ class VODStreamView(View):
except Exception:
return True
def _transform_url(self, content_obj, m3u_profile):
def _transform_url(self, original_url, m3u_profile):
"""Transform URL based on M3U profile settings"""
try:
import re
# Get URL from the content object's relations
original_url = None
if hasattr(content_obj, 'm3u_relations'):
# This is a Movie or Episode with relations
relation = content_obj.m3u_relations.filter(
m3u_account=m3u_profile.m3u_account
).first()
if relation:
original_url = relation.url if hasattr(relation, 'url') else relation.get_stream_url()
elif hasattr(content_obj, 'series'):
# This is an Episode, get URL from episode relation
from apps.vod.models import M3UEpisodeRelation
relation = M3UEpisodeRelation.objects.filter(
episode=content_obj,
m3u_account=m3u_profile.m3u_account
).first()
if relation:
original_url = relation.get_stream_url()
if not original_url:
logger.error("No URL found for content object")
return None
search_pattern = m3u_profile.search_pattern
@ -237,7 +232,7 @@ class VODStreamView(View):
except Exception as e:
logger.error(f"Error transforming URL: {e}")
return None
return original_url
@method_decorator(csrf_exempt, name='dispatch')
class VODPlaylistView(View):

View file

@ -128,6 +128,7 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
# Build response with available data
response_data = {
'id': movie.id,
'uuid': movie.uuid,
'stream_id': relation.stream_id,
'name': info.get('name', movie.name),
'o_name': info.get('o_name', ''),
@ -149,7 +150,7 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
'backdrop_path': (movie.custom_properties or {}).get('backdrop_path') or info.get('backdrop_path', []),
'cover': info.get('cover_big', ''),
'cover_big': info.get('cover_big', ''),
'movie_image': movie.logo.url or info.get('movie_image', ''),
'movie_image': movie.logo.url if movie.logo else info.get('movie_image', ''),
'bitrate': info.get('bitrate', 0),
'video': info.get('video', {}),
'audio': info.get('audio', {}),