mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-18 23:22:38 +02:00
Prepare RTLPlus for new streaming prov
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
# streaming_providers/providers/rtlplus/provider.py
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import ClassVar, Dict, List, Optional, Tuple
|
||||
@@ -63,7 +62,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
# Try authentication
|
||||
try:
|
||||
self.bearer_token = self.authenticator.get_bearer_token()
|
||||
logger.debug("RTL+ authentication successful during initialization")
|
||||
|
||||
# Ensure a profile is selected for user-authenticated sessions
|
||||
if self.authenticator.has_user_credentials():
|
||||
@@ -126,24 +124,14 @@ class RTLPlusProvider(StreamingProvider):
|
||||
if nb_pages is None:
|
||||
nb_pages = RTLPlusDefaults.DEFAULT_NB_PAGES
|
||||
|
||||
# 🔧 FIX: Clean up content_id based on layout_type
|
||||
original_content_id = content_id
|
||||
# Strip known type prefixes so the correct URL and cache key are used
|
||||
clean_content_id = content_id
|
||||
|
||||
if layout_type == "program" and content_id.startswith("program_"):
|
||||
# Remove "program_" prefix to get just the numeric ID
|
||||
clean_content_id = content_id[8:] # "program_68137" -> "68137"
|
||||
logger.debug(f"Cleaned program content_id: '{original_content_id}' -> '{clean_content_id}'")
|
||||
|
||||
clean_content_id = content_id[8:]
|
||||
elif layout_type == "folder" and content_id.startswith("folder_"):
|
||||
# Remove "folder_" prefix
|
||||
clean_content_id = content_id[7:]
|
||||
logger.debug(f"Cleaned folder content_id: '{original_content_id}' -> '{clean_content_id}'")
|
||||
|
||||
elif layout_type == "season" and content_id.startswith("season_"):
|
||||
# Remove "season_" prefix
|
||||
clean_content_id = content_id[7:]
|
||||
logger.debug(f"Cleaned season content_id: '{original_content_id}' -> '{clean_content_id}'")
|
||||
|
||||
# For video layouts, a prefix is always an error
|
||||
if layout_type == "video" and "_" in content_id and not content_id.startswith("clip"):
|
||||
@@ -159,7 +147,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
if not force_refresh and cache_key in self._layout_cache:
|
||||
cached_data, cached_time = self._layout_cache[cache_key]
|
||||
if (now - cached_time) < self.LAYOUT_CACHE_TTL:
|
||||
logger.debug(f"Layout cache hit: {cache_key}")
|
||||
return cached_data
|
||||
|
||||
# Get tokens
|
||||
@@ -167,7 +154,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
if not oauth_token:
|
||||
try:
|
||||
oauth_token = self.authenticator.get_bearer_token()
|
||||
logger.debug("Using anonymous token for layout")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get OAuth token for layout: {e}")
|
||||
return None
|
||||
@@ -184,31 +170,13 @@ class RTLPlusProvider(StreamingProvider):
|
||||
|
||||
# Build request - handle alias layouts specially
|
||||
if layout_type == "alias":
|
||||
# For alias layouts, the URL pattern is: {base}/alias/{content_id}/layout
|
||||
url = f"{self.rtl_config.bedrock_layout_base}/alias/{clean_content_id}/layout"
|
||||
else:
|
||||
# Use the cleaned content_id for the URL
|
||||
url = self.rtl_config.get_layout_url(layout_type, clean_content_id)
|
||||
|
||||
headers = self.rtl_config.get_layout_headers(oauth_token, bedrock_token, location)
|
||||
params = {"blockPage": block_page, "nbPages": nb_pages}
|
||||
|
||||
# LOG ALL HEADERS BEING SENT
|
||||
logger.debug(f"=== FETCH LAYOUT REQUEST HEADERS ===")
|
||||
logger.debug(f"URL: {url}")
|
||||
logger.debug(f"Method: GET")
|
||||
logger.debug(f"Headers:")
|
||||
# Mask sensitive tokens for security
|
||||
safe_headers = {}
|
||||
for key, value in headers.items():
|
||||
if 'authorization' in key.lower() or 'token' in key.lower():
|
||||
safe_headers[key] = f"{value[:20]}... (truncated)" if len(value) > 20 else "***"
|
||||
else:
|
||||
safe_headers[key] = value
|
||||
logger.debug(f"{json.dumps(safe_headers, indent=2)}")
|
||||
logger.debug(f"Params: {params}")
|
||||
logger.debug(f"==================================")
|
||||
|
||||
try:
|
||||
response = self.http_manager.get(
|
||||
url, headers=headers, params=params, operation="api"
|
||||
@@ -217,8 +185,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
data = response.json()
|
||||
|
||||
self._layout_cache[cache_key] = (data, now)
|
||||
logger.debug(f"Fetched {layout_type} layout for {clean_content_id}")
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
@@ -233,39 +199,20 @@ class RTLPlusProvider(StreamingProvider):
|
||||
assets = []
|
||||
blocks = layout_data.get("blocks", [])
|
||||
|
||||
logger.debug(f"extract_video_assets: Found {len(blocks)} blocks")
|
||||
|
||||
for block_idx, block in enumerate(blocks):
|
||||
block_type = block.get("type")
|
||||
logger.debug(f"Block {block_idx}: type={block_type}")
|
||||
|
||||
for block in blocks:
|
||||
if block.get("type") != "bffPaginated":
|
||||
logger.debug(f" Skipping block {block_idx} - not bffPaginated")
|
||||
continue
|
||||
|
||||
items = block.get("content", {}).get("items", [])
|
||||
logger.debug(f" Block {block_idx} has {len(items)} items")
|
||||
|
||||
for item_idx, item in enumerate(items):
|
||||
item_type = item.get("itemType")
|
||||
logger.debug(f" Item {item_idx}: itemType={item_type}")
|
||||
|
||||
for item in items:
|
||||
if item.get("itemType") == "video":
|
||||
video = item.get("itemContent", {}).get("video", {})
|
||||
video_assets = video.get("assets", [])
|
||||
logger.debug(f" Found {len(video_assets)} assets in video item")
|
||||
video_assets = item.get("itemContent", {}).get("video", {}).get("assets", [])
|
||||
assets.extend(video_assets)
|
||||
elif item.get("itemType") == "classic":
|
||||
# Some layouts wrap video inside classic items
|
||||
item_content = item.get("itemContent", {})
|
||||
video = item_content.get("video", {})
|
||||
video_assets = video.get("assets", [])
|
||||
logger.debug(f" Found {len(video_assets)} assets in classic item")
|
||||
video_assets = item.get("itemContent", {}).get("video", {}).get("assets", [])
|
||||
assets.extend(video_assets)
|
||||
else:
|
||||
logger.debug(f" Skipping item - unknown itemType: {item_type}")
|
||||
|
||||
logger.debug(f"extract_video_assets: Total assets found: {len(assets)}")
|
||||
return assets
|
||||
|
||||
@staticmethod
|
||||
@@ -325,22 +272,16 @@ class RTLPlusProvider(StreamingProvider):
|
||||
quality = preferred_quality or next(iter(self.rtl_config.preferred_qualities), "hd")
|
||||
format_pref = preferred_format or next(iter(self.rtl_config.preferred_formats), "dashcenc")
|
||||
|
||||
# Try exact match
|
||||
for asset in assets:
|
||||
if (asset.get("quality") == quality and
|
||||
asset.get("format") == format_pref):
|
||||
manifest_url = asset.get("path") or asset.get("reference")
|
||||
if manifest_url:
|
||||
return manifest_url
|
||||
# Try preferred format + quality first, then broaden search
|
||||
for fmt in (format_pref,):
|
||||
for qual in (quality,):
|
||||
for asset in assets:
|
||||
if asset.get("quality") == qual and asset.get("format") == fmt:
|
||||
manifest_url = asset.get("path") or asset.get("reference")
|
||||
if manifest_url:
|
||||
return manifest_url
|
||||
|
||||
# Try format + quality, any DRM
|
||||
for asset in assets:
|
||||
if asset.get("quality") == quality and asset.get("format") == format_pref:
|
||||
manifest_url = asset.get("path") or asset.get("reference")
|
||||
if manifest_url:
|
||||
return manifest_url
|
||||
|
||||
# Try by preferred formats/qualities
|
||||
# Try by all preferred formats/qualities
|
||||
for fmt in self.rtl_config.preferred_formats:
|
||||
for qual in self.rtl_config.preferred_qualities:
|
||||
for asset in assets:
|
||||
@@ -446,36 +387,25 @@ class RTLPlusProvider(StreamingProvider):
|
||||
- "program_68137" (we need to extract the clip_id from the program)
|
||||
- "program_68137/clip_1417600"
|
||||
"""
|
||||
logger.debug(f"get_manifest called with content_id={content_id}, kwargs={list(kwargs.keys())}")
|
||||
|
||||
# Check if we have a stored context from the VOD item
|
||||
# Kodi passes the program_id as content_id, but we stored the clip context
|
||||
if "program_context" in kwargs:
|
||||
# This should contain the clip_id we need
|
||||
if kwargs["program_context"].get("clip_id"):
|
||||
content_id = kwargs["program_context"]["clip_id"]
|
||||
logger.debug(f"Using clip_id from program_context: {content_id}")
|
||||
|
||||
# Handle combined paths
|
||||
if "/" in content_id:
|
||||
parts = content_id.split("/")
|
||||
if len(parts) == 2 and parts[0].startswith("program_"):
|
||||
content_id = parts[1] # clip_1417600
|
||||
logger.debug(f"Extracted clip_id from path: {content_id}")
|
||||
|
||||
# If content_id starts with program_, we need to find the clip_id from the program
|
||||
if content_id.startswith("program_"):
|
||||
program_id = content_id[8:] # Remove "program_" prefix
|
||||
logger.debug(f"Program ID detected: {program_id}, need to find clip_id")
|
||||
|
||||
# Fetch the program layout to find the clip_id
|
||||
layout = self._fetch_program_layout(program_id)
|
||||
if layout:
|
||||
# Find the video clip in the layout
|
||||
clip_id = self._find_clip_id_in_program_layout(layout)
|
||||
if clip_id:
|
||||
content_id = clip_id
|
||||
logger.debug(f"Found clip_id {clip_id} from program {program_id}")
|
||||
else:
|
||||
logger.error(f"No clip_id found for program {program_id}")
|
||||
return None
|
||||
@@ -498,7 +428,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
|
||||
def _fetch_program_layout(self, program_id: str) -> Optional[Dict]:
|
||||
"""Fetch a program layout by program ID."""
|
||||
# Ensure we have the right tokens
|
||||
oauth_token = self.get_user_bearer_token()
|
||||
if not oauth_token:
|
||||
logger.error("No user authentication for program layout")
|
||||
@@ -513,8 +442,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error(f"Failed to get Bedrock token: {e}")
|
||||
return None
|
||||
|
||||
# Build URL for program layout
|
||||
# The program ID should be numeric, not with "program_" prefix
|
||||
clean_id = program_id.replace("program_", "")
|
||||
url = f"{self.rtl_config.bedrock_layout_base}/program/{clean_id}/layout"
|
||||
location = f"{self.rtl_config.beta_website}p_{clean_id}-p_{clean_id}"
|
||||
@@ -543,9 +470,7 @@ class RTLPlusProvider(StreamingProvider):
|
||||
# Look for video assets in blocks
|
||||
assets = self.extract_video_assets(layout)
|
||||
if assets:
|
||||
# Return the first asset's ID or reference
|
||||
for asset in assets:
|
||||
# Look for clip_id in various fields
|
||||
clip_id = asset.get("clipId") or asset.get("id") or asset.get("reference")
|
||||
if clip_id:
|
||||
return clip_id
|
||||
@@ -575,26 +500,20 @@ class RTLPlusProvider(StreamingProvider):
|
||||
"""
|
||||
Get DRM configuration for content.
|
||||
"""
|
||||
logger.debug(f"get_drm called with content_id={content_id}")
|
||||
|
||||
# Handle combined paths and program IDs similar to get_manifest
|
||||
if "/" in content_id:
|
||||
parts = content_id.split("/")
|
||||
if len(parts) == 2 and parts[0].startswith("program_"):
|
||||
content_id = parts[1]
|
||||
logger.debug(f"Extracted clip_id from path for DRM: {content_id}")
|
||||
|
||||
# If content_id starts with program_, find the clip_id
|
||||
if content_id.startswith("program_"):
|
||||
program_id = content_id[8:]
|
||||
logger.debug(f"Program ID detected for DRM: {program_id}")
|
||||
|
||||
layout = self._fetch_program_layout(program_id)
|
||||
if layout:
|
||||
clip_id = self._find_clip_id_in_program_layout(layout)
|
||||
if clip_id:
|
||||
content_id = clip_id
|
||||
logger.debug(f"Found clip_id {clip_id} from program {program_id}")
|
||||
else:
|
||||
logger.error(f"No clip_id found for program {program_id} in DRM request")
|
||||
return []
|
||||
@@ -623,15 +542,10 @@ class RTLPlusProvider(StreamingProvider):
|
||||
@staticmethod
|
||||
def _is_linear_tv_channel(content_id: str) -> bool:
|
||||
"""Determine if content_id refers to a linear TV channel."""
|
||||
# VOD clips have patterns like "clip_123456" or contain "clip"
|
||||
if "clip_" in content_id or content_id.startswith("clip"):
|
||||
return False
|
||||
|
||||
# Also check for common VOD patterns
|
||||
if content_id.startswith("rrn:") or "/" in content_id:
|
||||
return False
|
||||
|
||||
# Event folder IDs are digits
|
||||
if content_id.isdigit():
|
||||
return False
|
||||
|
||||
@@ -646,13 +560,8 @@ class RTLPlusProvider(StreamingProvider):
|
||||
"""
|
||||
Get manifest URL for VOD/event content using Bedrock layout extraction.
|
||||
"""
|
||||
# If content_id doesn't start with "clip_", it might still be a clip
|
||||
# Make sure we're fetching the video layout, not live layout
|
||||
if not content_id.startswith("clip_"):
|
||||
# Try to find if this is a known clip ID pattern
|
||||
if content_id.isdigit():
|
||||
# This might be an event or program ID
|
||||
logger.warning(f"Non-clip content_id for manifest: {content_id}")
|
||||
if not content_id.startswith("clip_") and content_id.isdigit():
|
||||
logger.warning(f"Non-clip content_id for manifest: {content_id}")
|
||||
|
||||
# Get program context from kwargs (set when navigating from VOD categories)
|
||||
program_slug = kwargs.get("program_slug")
|
||||
@@ -660,18 +569,13 @@ class RTLPlusProvider(StreamingProvider):
|
||||
|
||||
# Build the correct location header
|
||||
if program_slug and program_id and content_id.startswith("clip_"):
|
||||
# Convert clip_1417600 to american-pie-c_1417600
|
||||
clip_slug = f"{program_slug}-c_{content_id.replace('clip_', '')}"
|
||||
location = f"{self.rtl_config.beta_website}{program_slug}-p_{program_id}/video/{clip_slug}"
|
||||
else:
|
||||
# Fallback - might not work for all content
|
||||
location = f"{self.rtl_config.beta_website}{content_id}"
|
||||
|
||||
logger.debug(f"Fetching video layout for content_id={content_id}, location={location}")
|
||||
|
||||
# Fetch the video layout - CRITICAL: use layout_type="video", not "live"!
|
||||
layout = self.fetch_layout(
|
||||
layout_type="video", # This must be "video" for VOD content
|
||||
layout_type="video",
|
||||
content_id=content_id,
|
||||
location=location
|
||||
)
|
||||
@@ -680,28 +584,23 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error(f"Failed to fetch video layout for {content_id}")
|
||||
return None
|
||||
|
||||
# Extract assets using the common method
|
||||
assets = self.extract_video_assets(layout)
|
||||
|
||||
if not assets:
|
||||
logger.warning(f"No video assets found for {content_id}")
|
||||
return None
|
||||
|
||||
# Select best manifest URL
|
||||
manifest_url = self.extract_best_manifest_url(assets)
|
||||
|
||||
if manifest_url:
|
||||
logger.info(f"Found manifest URL for {content_id}: {manifest_url[:100]}...")
|
||||
return manifest_url
|
||||
if not manifest_url:
|
||||
logger.warning(f"No suitable manifest URL found for {content_id}")
|
||||
|
||||
logger.warning(f"No suitable manifest URL found for {content_id}")
|
||||
return None
|
||||
return manifest_url
|
||||
|
||||
def _get_drm_vod_or_event(self, content_id: str, **kwargs) -> List[DRMConfig]:
|
||||
"""
|
||||
Get DRM configuration for VOD/event content using layout extraction.
|
||||
"""
|
||||
# Fetch layout for the content
|
||||
layout = self.fetch_layout(
|
||||
layout_type="video",
|
||||
content_id=content_id,
|
||||
@@ -712,14 +611,12 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error(f"Failed to fetch layout for VOD/event content_id: {content_id}")
|
||||
return []
|
||||
|
||||
# Use the common DRM extraction method
|
||||
return self.get_drm_for_content(layout)
|
||||
|
||||
def get_drm_for_content(self, layout_data: Dict) -> List[DRMConfig]:
|
||||
"""
|
||||
Extract DRM configuration from layout data.
|
||||
"""
|
||||
# Ensure profile is selected for user-authenticated sessions
|
||||
if self.authenticator.has_user_credentials():
|
||||
if not self.authenticator.ensure_profile_selected():
|
||||
logger.warning("Failed to select profile for DRM request")
|
||||
@@ -727,7 +624,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
assets = self.extract_video_assets(layout_data)
|
||||
|
||||
if not assets:
|
||||
logger.debug("No video assets found in layout")
|
||||
return []
|
||||
|
||||
# Priority: delta provider dashcenc format (best quality)
|
||||
@@ -739,7 +635,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
asset.get("format") == "dashcenc" and
|
||||
asset.get("quality") == "hd"):
|
||||
target_asset = asset
|
||||
logger.debug("Found delta/dashcenc/hd asset")
|
||||
break
|
||||
|
||||
# Fallback: delta + dashcenc + any quality
|
||||
@@ -747,7 +642,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
for asset in assets:
|
||||
if asset.get("provider") == "delta" and asset.get("format") == "dashcenc":
|
||||
target_asset = asset
|
||||
logger.debug(f"Fallback to delta/dashcenc/{asset.get('quality', 'unknown')} asset")
|
||||
break
|
||||
|
||||
# Last resort: any dashcenc asset
|
||||
@@ -755,14 +649,12 @@ class RTLPlusProvider(StreamingProvider):
|
||||
for asset in assets:
|
||||
if asset.get("format") == "dashcenc":
|
||||
target_asset = asset
|
||||
logger.debug(f"Last resort: {asset.get('provider', 'unknown')}/dashcenc asset")
|
||||
break
|
||||
|
||||
if not target_asset:
|
||||
logger.warning("No suitable DRM asset found")
|
||||
return []
|
||||
|
||||
# Extract contentId from the selected asset
|
||||
drm_info = target_asset.get("drm", {})
|
||||
drm_config = drm_info.get("config", {})
|
||||
content_id = drm_config.get("contentId")
|
||||
@@ -777,7 +669,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error("No user ID available for DRM")
|
||||
return []
|
||||
|
||||
# Get upfront token (works for both Widevine and PlayReady)
|
||||
upfront_token = self.authenticator.get_upfront_token(
|
||||
content_id=content_id,
|
||||
uid=uid,
|
||||
@@ -787,16 +678,14 @@ class RTLPlusProvider(StreamingProvider):
|
||||
logger.error(f"Failed to get upfront token for {content_id}")
|
||||
return []
|
||||
|
||||
# Use the shared DRMToday factory to create both configs
|
||||
drm_configs = create_drmtoday_configs(
|
||||
upfront_token=upfront_token,
|
||||
origin=self.rtl_config.beta_website.rstrip("/"),
|
||||
referer=self.rtl_config.beta_website,
|
||||
user_agent=self.rtl_config.user_agent,
|
||||
playready_user_agent=RTLPlusDefaults.PLAYREADY_USER_AGENT, # Edge for PlayReady
|
||||
playready_user_agent=RTLPlusDefaults.PLAYREADY_USER_AGENT,
|
||||
)
|
||||
|
||||
logger.info(f"Built {len(drm_configs)} DRM configs (Widevine + PlayReady)")
|
||||
return drm_configs
|
||||
|
||||
except Exception as e:
|
||||
@@ -810,7 +699,6 @@ class RTLPlusProvider(StreamingProvider):
|
||||
if cached is not None:
|
||||
data, ts = cached
|
||||
if (now - ts) < _MANIFEST_CACHE_TTL:
|
||||
logger.debug(f"RTL+ Manifest cache hit for {content_id}")
|
||||
return data
|
||||
|
||||
# TODO: This legacy endpoint may eventually be migrated to Bedrock
|
||||
@@ -829,11 +717,8 @@ class RTLPlusProvider(StreamingProvider):
|
||||
from ...base.auth.base_auth import TokenAuthLevel
|
||||
|
||||
current_level = self.authenticator.get_current_token_level()
|
||||
logger.debug(
|
||||
f"get_user_bearer_token: current_level={current_level}, has_user_credentials={self.authenticator.has_user_credentials()}")
|
||||
|
||||
if current_level == TokenAuthLevel.USER_AUTHENTICATED:
|
||||
# Ensure profile is selected
|
||||
self.authenticator.ensure_profile_selected()
|
||||
return self.authenticator.get_bearer_token()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ provider.get_manifest() / provider.get_drm() methods, which own
|
||||
the upfront-token flow and all format/quality selection logic.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
@@ -85,34 +86,28 @@ class RTLPlusVodManager:
|
||||
"program_<id>/clip_<id>" → direct clip (playable item)
|
||||
"clip_<id>" → direct clip (playable item)
|
||||
"""
|
||||
# NEW: Handle combined paths like "program_68137/clip_1417600"
|
||||
# Handle combined paths like "program_68137/clip_1417600"
|
||||
if "/" in content_id:
|
||||
parts = content_id.split("/")
|
||||
# Check if this is a program/clip combination
|
||||
if len(parts) == 2 and parts[0].startswith("program_") and parts[1].startswith("clip_"):
|
||||
# Extract the clip_id and return it as a direct playable item
|
||||
clip_id = parts[1]
|
||||
program_id = parts[0][8:] # Remove "program_" prefix
|
||||
|
||||
# For a direct clip, we can fetch and return it immediately
|
||||
# Since it's a leaf node, return it as a single-entry list
|
||||
vod_item = self._get_direct_clip_item(clip_id, program_id, **kwargs)
|
||||
if vod_item:
|
||||
logger.debug(f"Returning direct clip item: {vod_item.name} (ID: {vod_item.content_id})")
|
||||
return {"entries": [vod_item], "next_cursor": None, "total": 1}
|
||||
|
||||
# Fall through to normal handling
|
||||
content_id = clip_id
|
||||
|
||||
if not content_id:
|
||||
return self._get_root_category()
|
||||
|
||||
if content_id.startswith("folder_"):
|
||||
folder_id = content_id[7:] # "3"
|
||||
folder_id = content_id[7:]
|
||||
return self._get_folder_contents(folder_id, cursor, page_size)
|
||||
|
||||
if content_id.startswith("program_"):
|
||||
program_id = content_id[8:] # "68137" (numeric)
|
||||
program_id = content_id[8:]
|
||||
slug = kwargs.get("slug")
|
||||
return self._get_program_contents(program_id, cursor, page_size, slug=slug)
|
||||
|
||||
@@ -132,7 +127,6 @@ class RTLPlusVodManager:
|
||||
Fetch a direct clip item by its clip_id.
|
||||
This is used when we have a path like program_68137/clip_1417600.
|
||||
"""
|
||||
# Fetch the video layout for this clip
|
||||
layout = self._provider.fetch_layout(
|
||||
layout_type="video",
|
||||
content_id=clip_id,
|
||||
@@ -150,11 +144,9 @@ class RTLPlusVodManager:
|
||||
content_type="VOD",
|
||||
)
|
||||
|
||||
# Extract the VodItem from the layout
|
||||
vod_item = self._extract_vod_item_from_layout(layout, clip_id)
|
||||
|
||||
if not vod_item:
|
||||
# Extract from block items as fallback
|
||||
vod_item = self._extract_vod_item_from_layout_items(layout, clip_id)
|
||||
|
||||
return vod_item
|
||||
@@ -230,9 +222,8 @@ class RTLPlusVodManager:
|
||||
|
||||
def _get_root_category(self) -> Dict[str, Any]:
|
||||
try:
|
||||
entries: List[Union[VodCategory, VodItem]] = []
|
||||
folder_categories = [] # For folder types (main categories)
|
||||
program_categories = [] # For program types (shows/series)
|
||||
folder_categories = []
|
||||
program_categories = []
|
||||
future_events_count = 0
|
||||
|
||||
layout = self._provider.fetch_layout(
|
||||
@@ -245,13 +236,10 @@ class RTLPlusVodManager:
|
||||
logger.warning("Failed to fetch home layout for VOD root; returning empty")
|
||||
return {"entries": [], "next_cursor": None, "total": 0}
|
||||
|
||||
blocks = layout.get("blocks", [])
|
||||
|
||||
for block in blocks:
|
||||
for block in layout.get("blocks", []):
|
||||
if block.get("type") != "bffPaginated":
|
||||
continue
|
||||
|
||||
# Get items safely
|
||||
content = block.get("content")
|
||||
if not content:
|
||||
continue
|
||||
@@ -260,7 +248,6 @@ class RTLPlusVodManager:
|
||||
if not items:
|
||||
continue
|
||||
|
||||
# Process each item in the block
|
||||
for item in items:
|
||||
if not item:
|
||||
continue
|
||||
@@ -274,17 +261,14 @@ class RTLPlusVodManager:
|
||||
future_events_count += 1
|
||||
continue
|
||||
|
||||
# Try to extract as category first
|
||||
cat = self._extract_vod_category_from_block_item(item)
|
||||
if cat:
|
||||
# Separate folders from programs
|
||||
if cat.content_id.startswith("folder_"):
|
||||
folder_categories.append(cat)
|
||||
else:
|
||||
program_categories.append(cat)
|
||||
continue
|
||||
|
||||
# If not a category, try as VOD item
|
||||
vod_item = self._extract_vod_item_from_block_item(item)
|
||||
if vod_item:
|
||||
program_categories.append(vod_item)
|
||||
@@ -295,8 +279,10 @@ class RTLPlusVodManager:
|
||||
if future_events_count > 0:
|
||||
logger.info(f"Filtered out {future_events_count} future events from VOD root")
|
||||
|
||||
logger.info(f"Found {len(entries)} entries in root VOD category "
|
||||
f"({len(folder_categories)} folders, {len(program_categories)} programs)")
|
||||
logger.info(
|
||||
f"Found {len(entries)} entries in root VOD category "
|
||||
f"({len(folder_categories)} folders, {len(program_categories)} programs)"
|
||||
)
|
||||
|
||||
return {
|
||||
"entries": entries,
|
||||
@@ -359,16 +345,12 @@ class RTLPlusVodManager:
|
||||
"""
|
||||
program_id format: "68137" (numeric)
|
||||
"""
|
||||
# For API URL: use numeric ID directly
|
||||
api_program_id = program_id # "68137"
|
||||
|
||||
# For location header: use p_ prefix
|
||||
seo = slug or f"p_{program_id}"
|
||||
location = f"{self.cfg.beta_website}{seo}-p_{program_id}"
|
||||
|
||||
layout = self._provider.fetch_layout(
|
||||
layout_type="program",
|
||||
content_id=api_program_id, # "68137" (no p_ prefix)
|
||||
content_id=program_id,
|
||||
location=location,
|
||||
)
|
||||
|
||||
@@ -376,30 +358,15 @@ class RTLPlusVodManager:
|
||||
logger.error(f"Failed to fetch layout for program {program_id}")
|
||||
return {"entries": [], "next_cursor": None, "total": 0}
|
||||
|
||||
logger.debug(f"Layout type: {type(layout)}")
|
||||
if not isinstance(layout, dict):
|
||||
logger.error(f"Layout is not a dict, it's {type(layout)}")
|
||||
logger.error(f"Unexpected layout type for program {program_id}: {type(layout)}")
|
||||
return {"entries": [], "next_cursor": None, "total": 0}
|
||||
|
||||
blocks = layout.get("blocks", [])
|
||||
if blocks is None:
|
||||
logger.error(f"layout.get('blocks') returned None")
|
||||
blocks = []
|
||||
|
||||
logger.debug(f"Program {program_id} has {len(blocks)} blocks")
|
||||
for i, block in enumerate(blocks):
|
||||
if not block:
|
||||
logger.debug(f" Block {i}: is None")
|
||||
continue
|
||||
content = block.get("content", {}) if isinstance(block, dict) else {}
|
||||
content_template = content.get("contentTemplateId") if isinstance(content, dict) else None
|
||||
items = content.get("items", []) if isinstance(content, dict) else []
|
||||
logger.debug(f" Block {i}: type={block.get('type')}, template={content_template}, items={len(items)}")
|
||||
blocks = layout.get("blocks") or []
|
||||
|
||||
# FIRST: Check if this is a movie (direct playable video)
|
||||
movie_item = self._find_direct_video_in_layout(layout)
|
||||
if movie_item:
|
||||
logger.debug(f"Program {program_id} is a movie, returning playable item: {movie_item.name}")
|
||||
return {
|
||||
"entries": [movie_item],
|
||||
"next_cursor": None,
|
||||
@@ -419,11 +386,8 @@ class RTLPlusVodManager:
|
||||
|
||||
page = seasons[start: start + page_size]
|
||||
next_cursor = str(start + page_size) if (start + page_size) < len(seasons) else None
|
||||
|
||||
logger.debug(f"Program {program_id} has {len(seasons)} seasons")
|
||||
return {"entries": page, "next_cursor": next_cursor, "total": len(seasons)}
|
||||
|
||||
# No movie and no seasons found
|
||||
logger.warning(f"No seasons or movie found for program {program_id}")
|
||||
return {"entries": [], "next_cursor": None, "total": 0}
|
||||
|
||||
@@ -433,117 +397,49 @@ class RTLPlusVodManager:
|
||||
Searches through ALL blocks and items without filtering by template.
|
||||
"""
|
||||
if not layout or not isinstance(layout, dict):
|
||||
logger.error(f"_find_direct_video_in_layout: layout is {type(layout)}")
|
||||
return None
|
||||
|
||||
blocks = layout.get("blocks", [])
|
||||
if not blocks:
|
||||
logger.debug("No blocks found in layout")
|
||||
return None
|
||||
|
||||
logger.debug(f"Searching {len(blocks)} blocks for video content")
|
||||
|
||||
for block_idx, block in enumerate(blocks):
|
||||
for block in layout.get("blocks", []):
|
||||
if not block or not isinstance(block, dict):
|
||||
logger.debug(f"Block {block_idx} is invalid: {type(block)}")
|
||||
continue
|
||||
|
||||
block_type = block.get("type")
|
||||
logger.debug(f"Block {block_idx}: type={block_type}")
|
||||
|
||||
# Get items from standard location
|
||||
# Collect items from standard content location
|
||||
content = block.get("content", {})
|
||||
if not isinstance(content, dict):
|
||||
logger.debug(f" Block {block_idx} content is not a dict: {type(content)}")
|
||||
items = []
|
||||
else:
|
||||
items = content.get("items", [])
|
||||
if items is None:
|
||||
items = []
|
||||
items = list(content.get("items", []) or []) if isinstance(content, dict) else []
|
||||
|
||||
logger.debug(f" Found {len(items)} items in block.content")
|
||||
|
||||
# Also check alternative content for items
|
||||
# Also check alternativeContent for concurrent blocks
|
||||
alt_content = block.get("alternativeContent", {})
|
||||
if isinstance(alt_content, dict):
|
||||
concurrent_blocks = alt_content.get("concurrentBlocks", [])
|
||||
if concurrent_blocks:
|
||||
logger.debug(f" Found {len(concurrent_blocks)} concurrent blocks")
|
||||
for cb in concurrent_blocks:
|
||||
if isinstance(cb, dict):
|
||||
cb_content = cb.get("content", {})
|
||||
if isinstance(cb_content, dict):
|
||||
cb_items = cb_content.get("items", [])
|
||||
if cb_items:
|
||||
items.extend(cb_items)
|
||||
logger.debug(f" Added {len(cb_items)} items from concurrent block")
|
||||
for cb in alt_content.get("concurrentBlocks", []):
|
||||
if isinstance(cb, dict):
|
||||
cb_items = cb.get("content", {}).get("items", [])
|
||||
if cb_items:
|
||||
items.extend(cb_items)
|
||||
|
||||
for item_idx, item in enumerate(items):
|
||||
for item in items:
|
||||
if not item or not isinstance(item, dict):
|
||||
logger.debug(f" Item {item_idx} is invalid: {type(item)}")
|
||||
continue
|
||||
|
||||
item_type = item.get("itemType")
|
||||
logger.debug(f" Item {item_idx}: itemType={item_type}")
|
||||
|
||||
if item_type != "classic":
|
||||
logger.debug(f" Skipping - not classic type")
|
||||
if item.get("itemType") != "classic":
|
||||
continue
|
||||
|
||||
item_content = item.get("itemContent", {})
|
||||
if not item_content or not isinstance(item_content, dict):
|
||||
logger.debug(f" Skipping - no valid itemContent")
|
||||
continue
|
||||
|
||||
logger.debug(f" itemContent keys: {list(item_content.keys())}")
|
||||
|
||||
action = item_content.get("action", {})
|
||||
logger.debug(f" action type: {type(action)}")
|
||||
logger.debug(f" action value: {action}")
|
||||
|
||||
if isinstance(action, dict):
|
||||
target = action.get("target", {})
|
||||
logger.debug(f" target type: {type(target)}")
|
||||
logger.debug(f" target value: {target}")
|
||||
|
||||
if isinstance(target, dict):
|
||||
value_layout = target.get("value_layout", {})
|
||||
logger.debug(f" value_layout type: {type(value_layout)}")
|
||||
logger.debug(f" value_layout value: {value_layout}")
|
||||
|
||||
layout_type = value_layout.get("type") if isinstance(value_layout, dict) else None
|
||||
layout_id = value_layout.get("id") if isinstance(value_layout, dict) else None
|
||||
logger.debug(f" action.target.value_layout: type={layout_type}, id={layout_id}")
|
||||
|
||||
if layout_type == "video":
|
||||
logger.debug(f" Found video in action.target.value_layout")
|
||||
if isinstance(value_layout, dict) and value_layout.get("type") == "video":
|
||||
vod_item = self._extract_vod_item_from_block_item(item)
|
||||
if vod_item:
|
||||
# Make sure the content_id is the clip_id, not the program_id
|
||||
clip_id = value_layout.get("id") # This should be "clip_1417600"
|
||||
logger.debug(f" Setting VodItem content_id to: {clip_id}")
|
||||
vod_item.content_id = clip_id # Ensure it's set correctly
|
||||
|
||||
# Store program context for potential future use
|
||||
parent = value_layout.get("parent", {})
|
||||
# import json
|
||||
# vod_item.manifest_script = json.dumps({
|
||||
# "program_id": parent.get("id"),
|
||||
# "program_slug": parent.get("seo"),
|
||||
# "clip_id": clip_id
|
||||
# })
|
||||
|
||||
logger.debug(
|
||||
f" Successfully extracted VodItem: {vod_item.name} (ID: {vod_item.content_id})")
|
||||
clip_id = value_layout.get("id")
|
||||
vod_item.content_id = clip_id
|
||||
return vod_item
|
||||
else:
|
||||
logger.debug(f" Failed to extract VodItem from item")
|
||||
else:
|
||||
logger.debug(f" action is not a dict, it's {type(action)}")
|
||||
|
||||
# Check direct itemContent type
|
||||
if item_content.get("type") == "video":
|
||||
logger.debug(f" Found video in itemContent.type")
|
||||
clip_id = item_content.get("id")
|
||||
if clip_id:
|
||||
vod_item = VodItem.create_episode(
|
||||
@@ -553,15 +449,6 @@ class RTLPlusVodManager:
|
||||
)
|
||||
vod_item.description = item_content.get("description")
|
||||
vod_item.logo_url = self._extract_thumbnail(item_content)
|
||||
|
||||
# Store program context
|
||||
# import json
|
||||
# vod_item.manifest_script = json.dumps({
|
||||
# "clip_id": clip_id
|
||||
# })
|
||||
|
||||
logger.debug(
|
||||
f" Created VodItem from direct video: {vod_item.name} (ID: {vod_item.content_id})")
|
||||
return vod_item
|
||||
|
||||
# Check alternative action locations
|
||||
@@ -572,17 +459,13 @@ class RTLPlusVodManager:
|
||||
if isinstance(alt_target, dict):
|
||||
alt_value = alt_target.get("value_layout", {})
|
||||
if isinstance(alt_value, dict) and alt_value.get("type") == "video":
|
||||
logger.debug(f" Found video in {action_key}")
|
||||
vod_item = self._extract_vod_item_from_block_item(item)
|
||||
if vod_item:
|
||||
# Make sure content_id is the clip_id
|
||||
clip_id = alt_value.get("id")
|
||||
if clip_id:
|
||||
vod_item.content_id = clip_id
|
||||
logger.debug(f" Setting VodItem content_id from {action_key} to: {clip_id}")
|
||||
return vod_item
|
||||
|
||||
logger.debug("No video content found in any block")
|
||||
return None
|
||||
|
||||
def _extract_seasons_from_layout(self, layout: Dict) -> List[VodCategory]:
|
||||
@@ -600,7 +483,6 @@ class RTLPlusVodManager:
|
||||
alternative_content = block.get("alternativeContent")
|
||||
if alternative_content:
|
||||
for cb in alternative_content.get("concurrentBlocks", []):
|
||||
# Get season info from concurrent block
|
||||
season_title = cb.get("title", "Unbekannte Staffel")
|
||||
block_id = cb.get("id")
|
||||
|
||||
@@ -743,14 +625,11 @@ class RTLPlusVodManager:
|
||||
|
||||
if value_layout.get("type") == "video":
|
||||
clip_id = value_layout.get("id")
|
||||
# Get program context from parent if available
|
||||
parent = value_layout.get("parent", {})
|
||||
program_id = parent.get("id")
|
||||
program_slug = parent.get("seo")
|
||||
logger.debug(f" Extracted clip_id: {clip_id}, program_id: {program_id}, program_slug: {program_slug}")
|
||||
elif item_content.get("type") == "video":
|
||||
clip_id = item_content.get("id")
|
||||
logger.debug(f" Extracted clip_id from itemContent: {clip_id}")
|
||||
|
||||
# Try alternative action locations
|
||||
if not clip_id:
|
||||
@@ -762,16 +641,13 @@ class RTLPlusVodManager:
|
||||
alt_value = alt_target.get("value_layout", {})
|
||||
if isinstance(alt_value, dict) and alt_value.get("type") == "video":
|
||||
clip_id = alt_value.get("id")
|
||||
# Get program context from parent
|
||||
parent = alt_value.get("parent", {})
|
||||
program_id = parent.get("id")
|
||||
program_slug = parent.get("seo")
|
||||
logger.debug(f" Extracted clip_id from {action_key}: {clip_id}")
|
||||
if clip_id:
|
||||
break
|
||||
|
||||
if not clip_id:
|
||||
logger.debug(f" No clip_id found in item")
|
||||
return None
|
||||
|
||||
# Get name with fallback
|
||||
@@ -785,14 +661,9 @@ class RTLPlusVodManager:
|
||||
elif extra_title:
|
||||
full_title = extra_title
|
||||
else:
|
||||
# Try to get from highlight or other fields
|
||||
highlight = item_content.get("highlight", "")
|
||||
if highlight:
|
||||
# Clean up highlight (remove date info)
|
||||
if "•" in highlight:
|
||||
full_title = highlight.split("•")[0].strip()
|
||||
else:
|
||||
full_title = highlight
|
||||
full_title = highlight.split("•")[0].strip() if "•" in highlight else highlight
|
||||
else:
|
||||
full_title = f"Unbekanntes Video ({clip_id})"
|
||||
|
||||
@@ -801,12 +672,10 @@ class RTLPlusVodManager:
|
||||
episode_number: Optional[int] = None
|
||||
|
||||
if highlight:
|
||||
# Look for season/folge patterns
|
||||
season_match = re.search(r"Staffel\s*(\d+)", highlight, re.IGNORECASE)
|
||||
if season_match:
|
||||
season_number = int(season_match.group(1))
|
||||
else:
|
||||
# Try alternative: "S01" pattern
|
||||
season_match = re.search(r"S(\d+)", highlight, re.IGNORECASE)
|
||||
if season_match:
|
||||
season_number = int(season_match.group(1))
|
||||
@@ -815,15 +684,13 @@ class RTLPlusVodManager:
|
||||
if episode_match:
|
||||
episode_number = int(episode_match.group(1))
|
||||
else:
|
||||
# Try alternative: "E01" pattern
|
||||
episode_match = re.search(r"E(\d+)", highlight, re.IGNORECASE)
|
||||
if episode_match:
|
||||
episode_number = int(episode_match.group(1))
|
||||
|
||||
# Create VodItem with the clip_id as content_id
|
||||
vod_item = VodItem.create_episode(
|
||||
name=full_title,
|
||||
content_id=clip_id, # This should be "clip_1417600"
|
||||
content_id=clip_id,
|
||||
provider=self._provider.provider_name,
|
||||
season_number=season_number if season_number is not None else -1,
|
||||
episode_number=episode_number if episode_number is not None else -1,
|
||||
@@ -833,17 +700,14 @@ class RTLPlusVodManager:
|
||||
vod_item.duration_seconds = self._extract_duration(item_content)
|
||||
vod_item.progress = item_content.get("progress", 0)
|
||||
|
||||
# Store program context in manifest_script for later use (e.g., manifest fetching)
|
||||
# Store program context for manifest fetching
|
||||
if program_id or program_slug:
|
||||
import json
|
||||
vod_item.manifest_script = json.dumps({
|
||||
"program_id": program_id,
|
||||
"program_slug": program_slug,
|
||||
"clip_id": clip_id
|
||||
})
|
||||
logger.debug(f" Stored program context: program_id={program_id}, program_slug={program_slug}")
|
||||
|
||||
logger.debug(f" Created VodItem: '{vod_item.name}' with content_id='{vod_item.content_id}'")
|
||||
return vod_item
|
||||
|
||||
def _extract_vod_category_from_block_item(self, item: Dict) -> Optional[VodCategory]:
|
||||
@@ -874,38 +738,31 @@ class RTLPlusVodManager:
|
||||
|
||||
# Store with type prefix for clarity
|
||||
if layout_type == "folder":
|
||||
content_id = f"folder_{content_id}" # folder_3
|
||||
content_id = f"folder_{content_id}"
|
||||
elif layout_type == "program":
|
||||
content_id = f"program_{content_id}" # program_68137
|
||||
content_id = f"program_{content_id}"
|
||||
|
||||
# Capture the SEO slug from the API (e.g. "american-pie")
|
||||
seo_slug = value_layout.get("seo") or ""
|
||||
|
||||
# Get name with fallbacks
|
||||
name = item_content.get("title")
|
||||
|
||||
# If no title, try to get from folder mapping or other fields
|
||||
if not name and layout_type == "folder":
|
||||
if content_id in FOLDER_NAMES:
|
||||
name = FOLDER_NAMES[content_id]
|
||||
else:
|
||||
seo = value_layout.get("seo", "")
|
||||
if seo:
|
||||
name = seo.replace("-", " ").title()
|
||||
else:
|
||||
name = f"Kategorie {content_id}"
|
||||
name = seo.replace("-", " ").title() if seo else f"Kategorie {content_id}"
|
||||
elif not name:
|
||||
name = item_content.get("extraTitle") or item_content.get("highlight")
|
||||
if name:
|
||||
if "•" in str(name):
|
||||
name = name.split("•")[0].strip()
|
||||
if name and "•" in str(name):
|
||||
name = name.split("•")[0].strip()
|
||||
|
||||
if not name:
|
||||
name = f"Unbekannt {layout_type}"
|
||||
|
||||
return VodCategory(
|
||||
name=name,
|
||||
content_id=content_id, # "folder_3" or "program_68137"
|
||||
content_id=content_id,
|
||||
provider=self._provider.provider_name,
|
||||
logo_url=self._extract_thumbnail(item_content),
|
||||
description=item_content.get("description") or item_content.get("highlight"),
|
||||
@@ -965,25 +822,21 @@ class RTLPlusVodManager:
|
||||
if not highlight:
|
||||
return False
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
# Check for German date patterns with future dates
|
||||
date_patterns = [
|
||||
r"(\d{2})\.(\d{2})\.(\d{2}),?\s*(\d{2}):(\d{2})", # DD.MM.YY, HH:MM
|
||||
r"(\d{2})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2})", # DD.MM.YY HH:MM
|
||||
r"(\d{2})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2})", # DD.MM.YY HH:MM
|
||||
]
|
||||
|
||||
for pattern in date_patterns:
|
||||
match = re.search(pattern, highlight)
|
||||
if match:
|
||||
day, month, year, hour, minute = map(int, match.groups())
|
||||
# Convert 2-digit year to 4-digit
|
||||
if year < 100:
|
||||
year = 2000 + year
|
||||
try:
|
||||
event_date = datetime(year, month, day, hour, minute)
|
||||
# If date is in the future, this is an upcoming event
|
||||
if event_date > datetime.now():
|
||||
return True
|
||||
except ValueError:
|
||||
@@ -997,10 +850,8 @@ class RTLPlusVodManager:
|
||||
if not highlight:
|
||||
return False
|
||||
|
||||
# Check if it has date/time info
|
||||
has_datetime = bool(re.search(r"\d{2}\.\d{2}\.\d{2}", highlight))
|
||||
|
||||
# Check action target type
|
||||
action = item_content.get("action", {})
|
||||
target = action.get("target", {})
|
||||
value_layout = target.get("value_layout", {})
|
||||
@@ -1024,7 +875,6 @@ class RTLPlusVodManager:
|
||||
|
||||
if content_id.startswith("program_"):
|
||||
program_id = content_id[len("program_"):]
|
||||
# Program layouts are cached under "program:<id>:<page>:<nb>"
|
||||
cache_key = (
|
||||
f"program:{program_id}"
|
||||
f":{RTLPlusDefaults.DEFAULT_BLOCK_PAGE}"
|
||||
@@ -1052,8 +902,4 @@ class RTLPlusVodManager:
|
||||
|
||||
else:
|
||||
# Unknown prefix — wipe everything rather than silently doing nothing
|
||||
logger.debug(
|
||||
f"invalidate_cache: unknown content_id format {content_id!r}, "
|
||||
"clearing full layout cache"
|
||||
)
|
||||
self._provider.invalidate_layout_cache()
|
||||
Reference in New Issue
Block a user