mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-08 08:52:14 +02:00
97 lines
3.1 KiB
Python
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 |