Files
script.service.ultimate/lib/streaming_providers/providers/simpli/helpers.py
T
2026-10-04 11:26:53 +02:00

97 lines
3.1 KiB
Python

# streaming_providers/providers/simpli/helpers.py
"""
Helpers shared by more than one simpliTV module.
Public names on purpose: the template README requires that a helper
used by several modules is public (no leading underscore).
"""
import re
from contextlib import contextmanager
from datetime import datetime, timezone
from typing import Optional
from ...base.errors import ProviderError, ServerError
def prefer_dash(url: str) -> str:
"""
Rewrite an HLS manifest URL to its DASH equivalent.
Fallback only, and only for DRM-protected content. The
AcquireContent response normally carries both DASH (Type 9) and HLS
(Type 2) entries, and the channel manager picks the DASH one
directly when the content is protected; this function is called
only when the protected asset is HLS-only.
Never applied to unprotected content: the CDN has been serving the
HLS URL to the addon for those since day one, and swapping it out
has not been validated. Restart relies on a DVR seek offset from
the window start, and DASH time-shift semantics can differ from
HLS.
The replacement list is exactly the addon's forward direction; the
"nodrm" pair exists only in the addon's reverse (DASH -> HLS)
direction and must not be applied here.
"""
for old, new in (
(".m3u8", ".mpd"),
("/hls/", "/dash/"),
("hls_live", "dash_live"),
("/hls4h/", "/dash4h/"),
):
if old in url:
url = url.replace(old, new)
return url
@contextmanager
def transport_errors(what: str):
"""
Wrap *unexpected* failures (network, bad JSON) in ServerError while
letting typed provider errors through untouched.
Callers rely on AuthError / RateLimitError / GeoBlockError etc. to
refresh tokens or back off, so these must never be flattened into a
generic error.
"""
try:
yield
except ProviderError:
raise
except Exception as e:
raise ServerError(f"simpliTV: {what} failed: {e}") from e
# Fractional seconds followed by end-of-string, Z, or a UTC offset.
_FRACTION = re.compile(r"\.(\d+)(?=$|[Zz]|[+-]\d{2}(?::?\d{2})?$)")
_OFFSET_NO_COLON = re.compile(r"([+-]\d{2})(\d{2})$")
def parse_iso(value: Optional[str]) -> Optional[datetime]:
"""
Parse an ISO-8601 timestamp to a timezone-aware datetime, or None
if missing / malformed.
datetime.fromisoformat on Python < 3.11 accepts only 3 or 6
fractional digits and rejects "Z" and "+HHMM". .NET-style APIs
emit 7 digits, so the string is normalised first: fraction cut or
padded to 6 digits, "Z" -> "+00:00", "+HHMM" -> "+HH:MM". A
timestamp without an offset is read as UTC, never as local time.
"""
if not value or not isinstance(value, str):
return None
s = value.strip()
if not s:
return None
s = _FRACTION.sub(lambda m: "." + m.group(1)[:6].ljust(6, "0"), s)
if s[-1] in "Zz":
s = s[:-1] + "+00:00"
s = _OFFSET_NO_COLON.sub(r"\1:\2", s)
try:
dt = datetime.fromisoformat(s)
except (ValueError, TypeError):
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt