mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-22 00:52:36 +02:00
202 lines
6.6 KiB
Python
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 |