From 17d365f4d1fcf08f81831b05062056d7339353f0 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Sat, 2 May 2026 18:32:49 +0200 Subject: [PATCH] Prepare RTLPlus for new streaming prov --- .../providers/rtlplus/provider.py | 84 ++++++++----- .../providers/rtlplus/vod_manager.py | 118 +++++++++++++++--- 2 files changed, 152 insertions(+), 50 deletions(-) diff --git a/lib/streaming_providers/providers/rtlplus/provider.py b/lib/streaming_providers/providers/rtlplus/provider.py index 1864501..c34e9b2 100644 --- a/lib/streaming_providers/providers/rtlplus/provider.py +++ b/lib/streaming_providers/providers/rtlplus/provider.py @@ -440,7 +440,19 @@ class RTLPlusProvider(StreamingProvider): - Linear TV channels (via channel_manager) - VOD clips (via layout extraction) - Events (via event_manager) + + content_id may be: + - "clip_1417600" + - "program_68137/clip_1417600" (extract clip_id) """ + # NEW: Handle combined paths + 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_"): + content_id = parts[1] # Extract just the clip_id + logger.debug(f"Extracted clip_id from path: {content_id}") + # Try linear TV channel first if self._is_linear_tv_channel(content_id): return self.channel_manager.get_best_manifest_url(content_id) @@ -454,6 +466,48 @@ class RTLPlusProvider(StreamingProvider): # Fall back to VOD/event manifest extraction return self._get_manifest_vod_or_event(content_id, **kwargs) + def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]: + """ + Get DRM configuration for content. + + Supports: + - Linear TV channels (via channel_manager) + - VOD clips (via layout extraction) + - Events (via event_manager) + + content_id may be: + - "clip_1417600" + - "program_68137/clip_1417600" (extract clip_id) + """ + # NEW: Handle combined paths + 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_"): + content_id = parts[1] # Extract just the clip_id + logger.debug(f"Extracted clip_id from path for DRM: {content_id}") + + # Try linear TV channel first + if self._is_linear_tv_channel(content_id): + return self.channel_manager.get_drm_config_for_channel(content_id) + + # Try as event (folder) + if content_id.isdigit() and int(content_id) > 0: + drm_configs = self.event_manager.get_drm_for_event(content_id) + if drm_configs: + return drm_configs + + # Fall back to VOD DRM extraction + # This will also handle VOD clips and any other content types + drm_configs = self._get_drm_vod_or_event(content_id, **kwargs) + if drm_configs: + return drm_configs + + # If we get here, no DRM config could be found for any content type + logger.error( + f"No DRM configuration found for content_id: {content_id} (not a valid channel, event, or VOD item)") + return [] + @staticmethod def _is_linear_tv_channel(content_id: str) -> bool: """Determine if content_id refers to a linear TV channel.""" @@ -629,36 +683,6 @@ class RTLPlusProvider(StreamingProvider): logger.error(f"Failed to get DRM: {e}") return [] - def get_drm(self, content_id: str, **kwargs) -> List[DRMConfig]: - """ - Get DRM configuration for content. - - Supports: - - Linear TV channels (via channel_manager) - - VOD clips (via layout extraction) - - Events (via event_manager) - """ - # Try linear TV channel first - if self._is_linear_tv_channel(content_id): - return self.channel_manager.get_drm_config_for_channel(content_id) - - # Try as event (folder) - if content_id.isdigit() and int(content_id) > 0: - drm_configs = self.event_manager.get_drm_for_event(content_id) - if drm_configs: - return drm_configs - - # Fall back to VOD DRM extraction - # This will also handle VOD clips and any other content types - drm_configs = self._get_drm_vod_or_event(content_id, **kwargs) - if drm_configs: - return drm_configs - - # If we get here, no DRM config could be found for any content type - logger.error( - f"No DRM configuration found for content_id: {content_id} (not a valid channel, event, or VOD item)") - return [] - def _fetch_manifest_data(self, content_id: str) -> Optional[list]: """Fetch raw manifest data for VOD/events, with cache.""" now = time.monotonic() diff --git a/lib/streaming_providers/providers/rtlplus/vod_manager.py b/lib/streaming_providers/providers/rtlplus/vod_manager.py index aab6cdf..953c5e5 100644 --- a/lib/streaming_providers/providers/rtlplus/vod_manager.py +++ b/lib/streaming_providers/providers/rtlplus/vod_manager.py @@ -78,19 +78,32 @@ class RTLPlusVodManager: content_id conventions ---------------------- - "" → root (fetched from home layout) - "folder_" → Bedrock folder (e.g. "folder_3") - "program_" → program/series (e.g. "program_68137") - "season_" → season block (e.g. "season_abc-123") - - Returns - ------- - { - "entries": List[VodCategory | VodItem], - "next_cursor": Optional[str], - "total": Optional[int], - } + "" → root + "folder_" → Bedrock folder + "program_" → program/series + "season_" → season block + "program_/clip_" → direct clip (playable item) + "clip_" → direct clip (playable item) """ + # NEW: 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() @@ -114,6 +127,71 @@ class RTLPlusVodManager: logger.warning(f"Unrecognised VOD content_id format: {content_id!r}") return {"entries": [], "next_cursor": None, "total": 0} + def _get_direct_clip_item(self, clip_id: str, program_id: Optional[str] = None, **kwargs) -> Optional[VodItem]: + """ + 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, + location=f"{self.cfg.beta_website}{clip_id}", + ) + + if not layout: + logger.warning(f"Failed to fetch layout for direct clip {clip_id}") + # Fallback: create a basic VodItem with what we know + return VodItem( + name=kwargs.get("item_name", f"Video {clip_id}"), + content_id=clip_id, + provider=self._provider.provider_name, + mode="vod", + 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 + + def _extract_vod_item_from_layout_items(self, layout: Dict, clip_id: str) -> Optional[VodItem]: + """Extract VodItem from layout items (fallback method).""" + for block in layout.get("blocks", []): + if block.get("type") != "bffPaginated": + continue + for item in block.get("content", {}).get("items", []): + if item.get("itemType") != "classic": + continue + + item_content = item.get("itemContent", {}) + action = item_content.get("action", {}) + target = action.get("target", {}) + + # Handle lock-wrapped targets + if target.get("type") == "lock": + target = target.get("value_lock", {}).get("originalTarget", {}) + + value_layout = target.get("value_layout", {}) + + if value_layout.get("type") == "video" and value_layout.get("id") == clip_id: + vod_item = self._extract_vod_item_from_block_item(item) + if vod_item: + return vod_item + + # Also check direct itemContent + if item_content.get("type") == "video" and item_content.get("id") == clip_id: + return VodItem.create_episode( + name=item_content.get("title", clip_id), + content_id=clip_id, + provider=self._provider.provider_name, + ) + return None + def get_vod_item_info(self, clip_id: str) -> Optional[VodItem]: """ Return metadata for a single playable clip. @@ -756,14 +834,14 @@ class RTLPlusVodManager: vod_item.progress = item_content.get("progress", 0) # Store program context in manifest_script for later use (e.g., 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}") + 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