Files
script.service.ultimate/lib/streaming_providers/providers/rtlplus/layout_helpers.py
T
2026-05-18 10:53:45 +02:00

202 lines
6.6 KiB
Python

# streaming_providers/providers/rtlplus/layout_helpers.py
"""
RTL+ Layout Helpers
Pure, stateless utilities shared across event_manager, vod_manager, and
channel_manager. No I/O, no provider references — safe to import anywhere.
Centralises:
- Lock-target unwrapping (_unwrap_target)
- German date parsing (parse_german_datetime)
- Signed image URL construction (build_image_url, extract_thumbnail)
"""
import hashlib
import re
from datetime import datetime
from typing import Dict, Optional
from .constants import RTLPlusDefaults
# ---------------------------------------------------------------------------
# Lock-target unwrapping
# ---------------------------------------------------------------------------
def unwrap_target(target: Dict) -> Dict:
"""
Unwrap a lock-wrapped action target to its original target dict.
The Bedrock API wraps pay-walled or auth-gated targets like this::
{
"type": "lock",
"value_lock": {
"originalTarget": { "type": "...", "value_layout": {...} }
}
}
Calling this function on any target — locked or not — always returns the
"real" target so callers never need the ``if target.get("type") == "lock"``
boilerplate.
Args:
target: The raw ``action.target`` dict from a layout item.
Returns:
The unwrapped target dict, or the original dict if it wasn't locked.
"""
if not isinstance(target, dict):
return {}
if target.get("type") == "lock":
lock_value = target.get("value_lock", {})
if isinstance(lock_value, dict):
inner = lock_value.get("originalTarget", {})
if isinstance(inner, dict):
return inner
return target
# ---------------------------------------------------------------------------
# German date parsing
# ---------------------------------------------------------------------------
# Ordered from most-specific to least-specific so the first match wins.
_DATE_PATTERNS = [
# "Do., 07.05.26, 20:30 Uhr" (weekday, 2-digit year, with "Uhr")
r"[A-Za-z]+\.?,\s*(\d{2})\.(\d{2})\.(\d{2}),\s*(\d{2}):(\d{2})\s*Uhr",
# "Do., 07.05.26, 20:30" (weekday, 2-digit year, without "Uhr")
r"[A-Za-z]+\.?,\s*(\d{2})\.(\d{2})\.(\d{2}),\s*(\d{2}):(\d{2})",
# "07.05.26, 20:30 Uhr" (no weekday, 2-digit year, with "Uhr")
r"(\d{2})\.(\d{2})\.(\d{2}),\s*(\d{2}):(\d{2})\s*Uhr",
# "07.05.2026, 20:30" (no weekday, 4-digit year)
r"(\d{2})\.(\d{2})\.(\d{4}),\s*(\d{2}):(\d{2})",
# "07.05.26 20:30" (no comma)
r"(\d{2})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2})",
# "07.05.26" (date only, no time)
r"(\d{2})\.(\d{2})\.(\d{2,4})",
]
def parse_german_datetime(text: str) -> Optional[datetime]:
"""
Parse a German-formatted date/time string embedded in arbitrary text.
Handles all variants seen in RTL+ Bedrock highlight strings, e.g.::
"Fußball • Do., 07.05.26, 20:30 Uhr"
"Motorsport • Fr., 15.05.26, 13:10 Uhr"
"Motorsport \u2022 Sa., 16.05.26, 14:15 Uhr"
"07.05.2026, 20:30"
Two-digit years are expanded to the 21st century (``26`` → ``2026``).
Args:
text: Any string that may contain a German date.
Returns:
A ``datetime`` object, or ``None`` if no recognisable date was found.
"""
for pattern in _DATE_PATTERNS:
match = re.search(pattern, text)
if not match:
continue
groups = match.groups()
if len(groups) < 3:
continue
try:
day = int(groups[0])
month = int(groups[1])
year = int(groups[2])
# Expand 2-digit year
if year < 100:
year = 2000 + year
hour = int(groups[3]) if len(groups) > 3 else 0
minute = int(groups[4]) if len(groups) > 4 else 0
return datetime(year, month, day, hour, minute)
except ValueError:
continue # Try the next pattern
return None
# ---------------------------------------------------------------------------
# Signed image URL construction
# ---------------------------------------------------------------------------
def build_image_url(image_id: str, params: str = None) -> str:
"""
Build a signed Bedrock CDN image URL for the given image ID.
Hash formula: ``SHA1("/v2/images/{id}/raw?{params}" + IMAGE_SIGNING_KEY)``
The signing key is appended as a suffix (secret-suffix construction),
verified against multiple known-good URLs captured from network traces.
Args:
image_id: The raw image ID returned by the Bedrock API.
params: Optional query-string overriding ``RTLPlusDefaults.IMAGE_PARAMS``.
Use ``RTLPlusDefaults.IMAGE_PARAMS_LOGO`` for square channel logos.
Returns:
A fully-qualified, signed image URL.
"""
image_params = params if params is not None else RTLPlusDefaults.IMAGE_PARAMS
suffix = f"/{image_id}/raw?{image_params}"
signed_path = f"/v2/images{suffix}"
image_hash = hashlib.sha1(
(signed_path + RTLPlusDefaults.IMAGE_SIGNING_KEY).encode()
).hexdigest()
return f"{RTLPlusDefaults.IMAGE_BASE_URL}{suffix}&hash={image_hash}"
def extract_thumbnail(item_content: Dict) -> Optional[str]:
"""
Extract the best available thumbnail URL from a Bedrock item's content dict.
Tries aspect ratios in preference order: 16:9, 3:1, 1:1, 2:3.
Falls back to the top-level ``image.id`` if no ratio-keyed ID is found.
Args:
item_content: The ``itemContent`` dict from a Bedrock block item.
Returns:
A signed CDN URL, or ``None`` if no image data is present.
"""
image = item_content.get("image", {})
if not image:
return None
for ratio in ("16:9", "3:1", "1:1", "2:3"):
image_id = image.get("idsByRatio", {}).get(ratio)
if image_id:
return build_image_url(image_id)
image_id = image.get("id")
if image_id:
return build_image_url(image_id)
return None
def extract_thumbnail_from_layout(layout: Dict) -> Optional[str]:
"""
Extract a thumbnail URL from a top-level program/video layout dict.
Reads from ``layout.seo.image.id``, which is populated on program and
video layout responses but not on individual block items.
Args:
layout: A full Bedrock layout response dict.
Returns:
A signed CDN URL, or ``None`` if no SEO image is present.
"""
image_id = layout.get("seo", {}).get("image", {}).get("id")
if image_id:
return build_image_url(image_id)
return None