Stream VOD content (movies or series episodes) with session-based connection reuse Args: content_type: 'movie', 'series', or 'episode' content_id: ID of the content session_id: Optional session ID from URL path (for persistent connections) profile_id: Option
(request, content_type, content_id, session_id=None, profile_id=None, user=None)
| 388 | @authentication_classes([JWTAuthentication, ApiKeyAuthentication, QueryParamJWTAuthentication]) |
| 389 | @permission_classes([AllowAny]) |
| 390 | def stream_vod(request, content_type, content_id, session_id=None, profile_id=None, user=None): |
| 391 | """ |
| 392 | Stream VOD content (movies or series episodes) with session-based connection reuse |
| 393 | |
| 394 | Args: |
| 395 | content_type: 'movie', 'series', or 'episode' |
| 396 | content_id: ID of the content |
| 397 | session_id: Optional session ID from URL path (for persistent connections) |
| 398 | profile_id: Optional M3U profile ID for authentication |
| 399 | """ |
| 400 | if not network_access_allowed(request, "STREAMS"): |
| 401 | return JsonResponse({"error": "Forbidden"}, status=403) |
| 402 | if user is None and hasattr(request, "user") and request.user.is_authenticated: |
| 403 | user = request.user |
| 404 | logger.info(f"[VOD-REQUEST] Starting VOD stream request: {content_type}/{content_id}, session: {session_id}, profile: {profile_id}") |
| 405 | logger.info(f"[VOD-REQUEST] Full request path: {request.get_full_path()}") |
| 406 | logger.info(f"[VOD-REQUEST] Request method: {request.method}") |
| 407 | logger.info(f"[VOD-REQUEST] Request headers: {dict(request.headers)}") |
| 408 | |
| 409 | try: |
| 410 | client_ip, client_user_agent = get_client_info(request) |
| 411 | |
| 412 | # Extract timeshift parameters from query string |
| 413 | # Support multiple timeshift parameter formats |
| 414 | utc_start = request.GET.get('utc_start') or request.GET.get('start') or request.GET.get('playliststart') |
| 415 | utc_end = request.GET.get('utc_end') or request.GET.get('end') or request.GET.get('playlistend') |
| 416 | offset = request.GET.get('offset') or request.GET.get('seek') or request.GET.get('t') |
| 417 | |
| 418 | # VLC specific timeshift parameters |
| 419 | if not utc_start and not offset: |
| 420 | # Check for VLC-style timestamp parameters |
| 421 | if 'timestamp' in request.GET: |
| 422 | offset = request.GET.get('timestamp') |
| 423 | elif 'time' in request.GET: |
| 424 | offset = request.GET.get('time') |
| 425 | |
| 426 | # Session ID now comes from URL path parameter |
| 427 | # Remove legacy query parameter extraction since we're using path-based routing |
| 428 | |
| 429 | # Extract Range header for seeking support |
| 430 | range_header = request.META.get('HTTP_RANGE') |
| 431 | |
| 432 | logger.info(f"[VOD-TIMESHIFT] Timeshift params - utc_start: {utc_start}, utc_end: {utc_end}, offset: {offset}") |
| 433 | logger.info(f"[VOD-SESSION] Session ID: {session_id}") |
| 434 | |
| 435 | # Log all query parameters for debugging |
| 436 | if request.GET: |
| 437 | logger.debug(f"[VOD-PARAMS] All query params: {dict(request.GET)}") |
| 438 | |
| 439 | if range_header: |
| 440 | logger.info(f"[VOD-RANGE] Range header: {range_header}") |
| 441 | |
| 442 | # Parse the range to understand what position VLC is seeking to |
| 443 | try: |
| 444 | if 'bytes=' in range_header: |
| 445 | range_part = range_header.replace('bytes=', '') |
| 446 | if '-' in range_part: |
| 447 | start_byte, end_byte = range_part.split('-', 1) |